From de0050f4d6a7e2b5f1cddcb3e6f307878e1b5bdf Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 12 May 2017 11:39:16 +0800 Subject: [PATCH 01/67] update gradle, preparing for v2dev --- common/build.gradle | 21 +++++++++++++++ common/gradle.properties | 3 +++ gradle.properties | 11 +++----- helper/build.gradle | 4 +-- helper/gradle.properties | 4 +-- plugin/build.gradle | 7 ++--- plugin/gradle.properties | 2 +- sample/build.gradle | 27 ++++++++++--------- sample/gradle.properties | 5 +++- .../packer/samples/ApplicationTest.java | 13 --------- settings.gradle | 1 + 11 files changed, 55 insertions(+), 43 deletions(-) create mode 100644 common/build.gradle create mode 100644 common/gradle.properties delete mode 100644 sample/src/androidTest/java/com/mcxiaoke/packer/samples/ApplicationTest.java diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000..6994760 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,21 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + } +} + +repositories { + jcenter() +} + +apply plugin: 'java' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +dependencies { +} + +apply from: '../gradle-mvn-push.gradle' diff --git a/common/gradle.properties b/common/gradle.properties new file mode 100644 index 0000000..8fc506b --- /dev/null +++ b/common/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=common +POM_PACKAGING=jar +POM_NAME=Common Classes for Packer-Ng Plugin diff --git a/gradle.properties b/gradle.properties index 29e4ff5..25ea85a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ -VERSION_NAME=1.0.9 -VERSION_CODE=109 +VERSION_NAME=1.1.0-SNAPSHIT +VERSION_CODE=110 -GROUP=com.mcxiaoke.gradle +GROUP=com.mcxiaoke.packerng POM_DESCRIPTION=Next Generation Android Multi Market Packer Gradle Plugin POM_URL=https://github.com/mcxiaoke/packer-ng-plugin @@ -14,8 +14,3 @@ POM_LICENCE_DIST=repo POM_DEVELOPER_ID=mcxiaoke POM_DEVELOPER_NAME=Xiaoke Zhang POM_DEVELOPER_EMAIL=packer-ng-plugin@mcxiaoke.com - -MIN_SDK_VERSION=15 -TARGET_SDK_VERSION=24 -COMPILE_SDK_VERSION=24 -BUILD_TOOLS_VERSION=24.0.1 diff --git a/helper/build.gradle b/helper/build.gradle index 380e303..09c2de7 100644 --- a/helper/build.gradle +++ b/helper/build.gradle @@ -12,8 +12,8 @@ repositories { apply plugin: 'java' -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.7 +targetCompatibility = 1.7 dependencies { } diff --git a/helper/gradle.properties b/helper/gradle.properties index b838a09..7060573 100644 --- a/helper/gradle.properties +++ b/helper/gradle.properties @@ -1,3 +1,3 @@ -POM_ARTIFACT_ID=packer-helper +POM_ARTIFACT_ID=helper POM_PACKAGING=jar -POM_NAME=Helper Classes for Android Market Packer-Ng +POM_NAME=Helper Classes for Packer-Ng Android diff --git a/plugin/build.gradle b/plugin/build.gradle index 7f36cab..253d1e4 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'groovy' -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.7 +targetCompatibility = 1.7 buildscript { repositories { @@ -17,7 +17,8 @@ dependencies { compile localGroovy() compile gradleApi() compile project(':helper') - compile 'com.android.tools.build:gradle:2.2.1' + compile 'com.android.tools.build:gradle:2.2.2' + compile 'com.android.tools.build:apksig:2.3.0' } apply from: '../gradle-mvn-push.gradle' diff --git a/plugin/gradle.properties b/plugin/gradle.properties index cf559c2..f6454c3 100644 --- a/plugin/gradle.properties +++ b/plugin/gradle.properties @@ -1,3 +1,3 @@ -POM_ARTIFACT_ID=packer-ng +POM_ARTIFACT_ID=plugin POM_PACKAGING=jar POM_NAME=Next Generation Android Market Packer diff --git a/sample/build.gradle b/sample/build.gradle index d6b0547..f49df1e 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,14 +1,14 @@ buildscript { - ext.packer_version = '1.0.9-SNAPSHOT' + ext.packer_version = '1.0.9' repositories { maven { url '/tmp/repo/' } jcenter() - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } +// maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } dependencies { - classpath "com.android.tools.build:gradle:2.2.1" + classpath "com.android.tools.build:gradle:2.2.2" classpath "com.mcxiaoke.gradle:packer-ng:$packer_version" } } @@ -16,7 +16,7 @@ buildscript { repositories { maven { url '/tmp/repo/' } jcenter() - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } +// maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } apply plugin: 'com.android.application' @@ -27,14 +27,10 @@ dependencies { // compile project(':helper') compile "com.mcxiaoke.gradle:packer-helper:$packer_version" compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:support-v4:24.1.1' - compile 'com.android.support:appcompat-v7:24.1.1' + compile 'com.android.support:support-v4:25.3.1' + compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.jakewharton:butterknife:6.0.0' - compile('com.mcxiaoke.next:core:1.2.0@aar') { - exclude group: 'com.android.support', module: 'support-v4' - } - compile('com.mcxiaoke.next:http:1.2.0@aar') - compile('com.mcxiaoke.next:ui:1.2.0@aar') { + compile('com.mcxiaoke.next:core:1.5.0') { exclude group: 'com.android.support', module: 'support-v4' } } @@ -47,11 +43,16 @@ packer { } android { + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + encoding "UTF-8" + } + compileSdkVersion Integer.parseInt(project.COMPILE_SDK_VERSION) buildToolsVersion project.BUILD_TOOLS_VERSION - compileOptions.encoding = "UTF-8" - defaultConfig { versionName project.VERSION_NAME versionCode Integer.parseInt(project.VERSION_CODE) diff --git a/sample/gradle.properties b/sample/gradle.properties index 1a013dc..26bd099 100644 --- a/sample/gradle.properties +++ b/sample/gradle.properties @@ -1 +1,4 @@ -#market=markets.txt +MIN_SDK_VERSION=15 +TARGET_SDK_VERSION=25 +COMPILE_SDK_VERSION=25 +BUILD_TOOLS_VERSION=25.0.3 diff --git a/sample/src/androidTest/java/com/mcxiaoke/packer/samples/ApplicationTest.java b/sample/src/androidTest/java/com/mcxiaoke/packer/samples/ApplicationTest.java deleted file mode 100644 index b2d69ec..0000000 --- a/sample/src/androidTest/java/com/mcxiaoke/packer/samples/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.mcxiaoke.packer.samples; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index be487dc..70ea61b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ +include ':common' include ':plugin' include ':helper' include ':sample' From 645f73b7bc4e88fc9ee10ec80a12ca0f5f0ae6b6 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 26 May 2017 18:21:31 +0800 Subject: [PATCH 02/67] add new v2 signature read and write module, include tests --- common/build.gradle | 16 + .../mcxiaoke/packer/common/PackerParser.java | 61 ++++ .../mcxiaoke/packer/common/PayloadUtils.java | 103 +++++++ .../packer/support/walle/ApkSigningBlock.java | 107 +++++++ .../support/walle/ApkSigningPayload.java | 30 ++ .../packer/support/walle/ApkUtil.java | 273 ++++++++++++++++++ .../mcxiaoke/packer/support/walle/Pair.java | 60 ++++ .../packer/support/walle/PayloadReader.java | 49 ++++ .../packer/support/walle/PayloadWriter.java | 130 +++++++++ .../packer/support/walle/V2Const.java | 39 +++ .../packer/support/walle/V2Utils.java | 29 ++ .../mcxiaoke/packer/common/PayloadTests.java | 232 +++++++++++++++ .../com/mcxiaoke/packer/common/TestUtils.java | 104 +++++++ 13 files changed, 1233 insertions(+) create mode 100644 common/src/main/java/com/mcxiaoke/packer/common/PackerParser.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/common/PayloadUtils.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/support/walle/ApkSigningBlock.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/support/walle/ApkSigningPayload.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/support/walle/ApkUtil.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/support/walle/Pair.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadReader.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadWriter.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/support/walle/V2Const.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/support/walle/V2Utils.java create mode 100644 common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java create mode 100644 common/src/test/java/com/mcxiaoke/packer/common/TestUtils.java diff --git a/common/build.gradle b/common/build.gradle index 6994760..f8f0182 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -16,6 +16,22 @@ sourceCompatibility = 1.7 targetCompatibility = 1.7 dependencies { + // JUnit and Mockito + testCompile "junit:junit:4.12" + testCompile "org.mockito:mockito-core:1.10.19" + testCompile "commons-io:commons-io:2.5" + testCompile 'com.android.tools.build:apksig:2.3.0' + testCompile "com.mcxiaoke.next:core:1.5.0" } +test { + testLogging.showStandardStreams = true +} + +javadoc { + failOnError false +} + + + apply from: '../gradle-mvn-push.gradle' diff --git a/common/src/main/java/com/mcxiaoke/packer/common/PackerParser.java b/common/src/main/java/com/mcxiaoke/packer/common/PackerParser.java new file mode 100644 index 0000000..26fbd27 --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/common/PackerParser.java @@ -0,0 +1,61 @@ +package com.mcxiaoke.packer.common; + +import java.io.File; +import java.io.IOException; + +/** + * User: mcxiaoke + * Date: 2017/5/17 + * Time: 15:39 + */ +public class PackerParser { + + + public static PackerParser create(File apkFile) { + return new PackerParser(apkFile); + } + + public static PackerParser create(File apkFile, String channelKey) { + return new PackerParser(apkFile, channelKey); + } + + public static PackerParser create(File apkFile, String channelKey, int channelBlockId) { + return new PackerParser(apkFile, channelKey, channelBlockId); + } + + // channel info key + public static final String DEFAULT_CHANNEL_KEY = "0x4d6975"; + // channel info id + public static final int DEFAULT_CHANNEL_BLOCK_ID = 0x717a786b; + + + private File apkFile; + private String channelKey; + private int channelBlockId; + + PackerParser(final File apkFile) { + this(apkFile, DEFAULT_CHANNEL_KEY, DEFAULT_CHANNEL_BLOCK_ID); + } + + PackerParser(final File apkFile, final String channelKey) { + this(apkFile, channelKey, DEFAULT_CHANNEL_BLOCK_ID); + } + + PackerParser(final File apkFile, + final String channelKey, + final int channelBlockId) { + this.apkFile = apkFile; + this.channelKey = channelKey; + this.channelBlockId = channelBlockId; + } + + public String readChannel() throws IOException { + return PayloadUtils.readChannel(apkFile, channelKey, channelBlockId); + } + + public void writeChannel(final String channel) throws IOException { + PayloadUtils.writeChannel(apkFile, channel, channelKey, channelBlockId); + } + + +} diff --git a/common/src/main/java/com/mcxiaoke/packer/common/PayloadUtils.java b/common/src/main/java/com/mcxiaoke/packer/common/PayloadUtils.java new file mode 100644 index 0000000..9ad4538 --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/common/PayloadUtils.java @@ -0,0 +1,103 @@ +package com.mcxiaoke.packer.common; + +import com.mcxiaoke.packer.support.walle.PayloadReader; +import com.mcxiaoke.packer.support.walle.PayloadWriter; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * User: mcxiaoke + * Date: 2017/5/26 + * Time: 13:18 + */ +public class PayloadUtils { + // charset utf8 + public static final String UTF8 = "UTF-8"; + + public static String readChannel(File apkFile, + String channelKey, + int blockId) throws IOException { + final Map map = readValues(apkFile, blockId); + if (map == null || map.isEmpty()) { + return null; + } + return map.get(channelKey); + } + + public static void writeChannel(File apkFile, + String channel, + String channelKey, + int blockId) throws IOException { + final Map values = new HashMap<>(); + values.put(channelKey, channel); + writeValues(apkFile, values, blockId); + } + + public static Map readValues(File apkFile, int blockId) + throws IOException { + final String content = readRaw(apkFile, blockId); + return mapFromString(content); + } + + public static String readRaw(File apkFile, int blockId) throws IOException { + final byte[] bytes = PayloadReader.readBlock(apkFile, blockId); + if (bytes == null || bytes.length == 0) { + return null; + } + return new String(bytes, UTF8); + } + + public static void writeValues(File apkFile, Map values, int blockId) + throws IOException { + if (values == null || values.isEmpty()) { + return; + } + final Map newValues = new HashMap<>(); + final Map oldValues = readValues(apkFile, blockId); + if (oldValues != null) { + newValues.putAll(oldValues); + } + newValues.putAll(values); + writeRaw(apkFile, mapToString(newValues), blockId); + } + + public static void writeRaw(File apkFile, final String content, int blockId) + throws IOException { + PayloadWriter.writeBlock(apkFile, blockId, content.getBytes(UTF8)); + } + + private static final String SEP_KV = "\u2218"; + private static final String SEP_LINE = "\u2219"; + + private static String mapToString(final Map map) throws IOException { + if (map == null || map.isEmpty()) { + return null; + } + + final StringBuilder builder = new StringBuilder(); + for (Entry entry : map.entrySet()) { + builder.append(entry.getKey()).append(SEP_KV) + .append(entry.getValue()).append(SEP_LINE); + } + return builder.toString(); + } + + private static Map mapFromString(final String string) { + if (string == null || string.length() == 0) { + return null; + } + final Map map = new HashMap<>(); + final String[] entries = string.split(SEP_LINE); + for (String entry : entries) { + final String[] kv = entry.split(SEP_KV); + if (kv.length == 2) { + map.put(kv[0], kv[1]); + } + } + return map; + } +} diff --git a/common/src/main/java/com/mcxiaoke/packer/support/walle/ApkSigningBlock.java b/common/src/main/java/com/mcxiaoke/packer/support/walle/ApkSigningBlock.java new file mode 100644 index 0000000..988811d --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/support/walle/ApkSigningBlock.java @@ -0,0 +1,107 @@ +package com.mcxiaoke.packer.support.walle; + +import java.io.DataOutput; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; + +/** + * https://source.android.com/security/apksigning/v2.html + * https://en.wikipedia.org/wiki/Zip_(file_format) + */ +class ApkSigningBlock { + // The format of the APK Signing Block is as follows (all numeric fields are little-endian): + + // .size of block in bytes (excluding this field) (uint64) + // .Sequence of uint64-length-prefixed ID-value pairs: + // *ID (uint32) + // *value (variable-length: length of the pair - 4 bytes) + // .size of block in bytes—same as the very first field (uint64) + // .magic “APK Sig Block 42” (16 bytes) + + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes payload + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + + // payload 有 8字节的大小,4字节的ID,还有payload的内容组成 + + private final List payloads; + + ApkSigningBlock() { + super(); + payloads = new ArrayList(); + } + + public final List getPayloads() { + return payloads; + } + + public void addPayload(final ApkSigningPayload payload) { + payloads.add(payload); + } + + /** + * @param dataOutput DataOutput + * @return ApkSigningBlock length + * @throws IOException IOException + */ + public long writeTo(final DataOutput dataOutput) throws IOException { + long length = 24; // 24 = 8(size of block in bytes + // same as the very first field (uint64)) + 16 (magic “APK Sig Block 42” (16 bytes)) + for (int index = 0; index < payloads.size(); ++index) { + final ApkSigningPayload payload = payloads.get(index); + final byte[] bytes = payload.getByteBuffer(); + length += 12 + bytes.length; // 12 = 8(uint64-length-prefixed) + 4 (ID (uint32)) + } + + ByteBuffer byteBuffer = ByteBuffer.allocate(8); // Long.BYTES + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putLong(length); + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + for (int index = 0; index < payloads.size(); ++index) { + final ApkSigningPayload payload = payloads.get(index); + final byte[] bytes = payload.getByteBuffer(); + + byteBuffer = ByteBuffer.allocate(8); // Long.BYTES + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putLong(bytes.length + (8 - 4)); // Long.BYTES - Integer.BYTES + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + byteBuffer = ByteBuffer.allocate(4); // Integer.BYTES + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putInt(payload.getId()); + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + dataOutput.write(bytes); + } + + byteBuffer = ByteBuffer.allocate(8); // Long.BYTES + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putLong(length); + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + byteBuffer = ByteBuffer.allocate(8); // Long.BYTES + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putLong(V2Const.APK_SIG_BLOCK_MAGIC_LO); + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + byteBuffer = ByteBuffer.allocate(8); // Long.BYTES + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putLong(V2Const.APK_SIG_BLOCK_MAGIC_HI); + byteBuffer.flip(); + dataOutput.write(byteBuffer.array()); + + return length; + } +} diff --git a/common/src/main/java/com/mcxiaoke/packer/support/walle/ApkSigningPayload.java b/common/src/main/java/com/mcxiaoke/packer/support/walle/ApkSigningPayload.java new file mode 100644 index 0000000..c371833 --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/support/walle/ApkSigningPayload.java @@ -0,0 +1,30 @@ +package com.mcxiaoke.packer.support.walle; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +class ApkSigningPayload { + private final int id; + private final ByteBuffer buffer; + + ApkSigningPayload(final int id, final ByteBuffer buffer) { + super(); + this.id = id; + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + this.buffer = buffer; + } + + public int getId() { + return id; + } + + public byte[] getByteBuffer() { + final byte[] array = buffer.array(); + final int arrayOffset = buffer.arrayOffset(); + return Arrays.copyOfRange(array, arrayOffset + buffer.position(), + arrayOffset + buffer.limit()); + } +} diff --git a/common/src/main/java/com/mcxiaoke/packer/support/walle/ApkUtil.java b/common/src/main/java/com/mcxiaoke/packer/support/walle/ApkUtil.java new file mode 100644 index 0000000..e36a630 --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/support/walle/ApkUtil.java @@ -0,0 +1,273 @@ +package com.mcxiaoke.packer.support.walle; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.util.LinkedHashMap; +import java.util.Map; + +final class ApkUtil { + private ApkUtil() { + super(); + } + + public static long findZipCommentLength(final FileChannel fileChannel) throws IOException { + // End of central directory record (EOCD) + // Offset Bytes Description[23] + // 0 4 End of central directory signature = 0x06054b50 + // 4 2 Number of this disk + // 6 2 Disk where central directory starts + // 8 2 Number of central directory records on this disk + // 10 2 Total number of central directory records + // 12 4 Size of central directory (bytes) + // 16 4 Offset of start of central directory, relative to start of archive + // 20 2 Comment length (n) + // 22 n Comment + // For a zip with no archive comment, the + // end-of-central-directory record will be 22 bytes long, so + // we expect to find the EOCD marker 22 bytes from the end. + + + final long archiveSize = fileChannel.size(); + if (archiveSize < V2Const.ZIP_EOCD_REC_MIN_SIZE) { + throw new IOException("APK too small for ZIP End of Central Directory (EOCD) record"); + } + // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. + // The record can be identified by its 4-byte signature/magic which is located at the very + // beginning of the record. A complication is that the record is variable-length because of + // the comment field. + // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from + // end of the buffer for the EOCD record signature. Whenever we find a signature, we check + // the candidate record's comment length is such that the remainder of the record takes up + // exactly the remaining bytes in the buffer. The search is bounded because the maximum + // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. + final long maxCommentLength = Math.min(archiveSize - V2Const.ZIP_EOCD_REC_MIN_SIZE, V2Const.UINT16_MAX_VALUE); + final long eocdWithEmptyCommentStartPosition = archiveSize - V2Const.ZIP_EOCD_REC_MIN_SIZE; + for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; + expectedCommentLength++) { + final long eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; + + final ByteBuffer byteBuffer = ByteBuffer.allocate(4); + fileChannel.position(eocdStartPos); + fileChannel.read(byteBuffer); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + if (byteBuffer.getInt(0) == V2Const.ZIP_EOCD_REC_SIG) { + final ByteBuffer commentLengthByteBuffer = ByteBuffer.allocate(2); + fileChannel.position(eocdStartPos + V2Const.ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); + fileChannel.read(commentLengthByteBuffer); + commentLengthByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + final int actualCommentLength = commentLengthByteBuffer.getShort(0); + if (actualCommentLength == expectedCommentLength) { + return actualCommentLength; + } + } + } + throw new IOException("ZIP End of Central Directory (EOCD) record not found"); + } + + public static long findCentralDirStartOffset(final FileChannel fileChannel) throws IOException { + return findCentralDirStartOffset(fileChannel, findZipCommentLength(fileChannel)); + } + + public static long findCentralDirStartOffset(final FileChannel fileChannel, final long commentLength) throws IOException { + // End of central directory record (EOCD) + // Offset Bytes Description[23] + // 0 4 End of central directory signature = 0x06054b50 + // 4 2 Number of this disk + // 6 2 Disk where central directory starts + // 8 2 Number of central directory records on this disk + // 10 2 Total number of central directory records + // 12 4 Size of central directory (bytes) + // 16 4 Offset of start of central directory, relative to start of archive + // 20 2 Comment length (n) + // 22 n Comment + // For a zip with no archive comment, the + // end-of-central-directory record will be 22 bytes long, so + // we expect to find the EOCD marker 22 bytes from the end. + + final ByteBuffer zipCentralDirectoryStart = ByteBuffer.allocate(4); + zipCentralDirectoryStart.order(ByteOrder.LITTLE_ENDIAN); + fileChannel.position(fileChannel.size() - commentLength - 6); // 6 = 2 (Comment length) + 4 (Offset of start of central directory, relative to start of archive) + fileChannel.read(zipCentralDirectoryStart); + final long centralDirStartOffset = zipCentralDirectoryStart.getInt(0); + return centralDirStartOffset; + } + + public static Pair findApkSigningBlock( + final FileChannel fileChannel) throws IOException { + final long centralDirOffset = findCentralDirStartOffset(fileChannel); + return findApkSigningBlock(fileChannel, centralDirOffset); + } + + public static Pair findApkSigningBlock( + final FileChannel fileChannel, final long centralDirOffset) throws IOException { + + // Find the APK Signing Block. The block immediately precedes the Central Directory. + + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes payload + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + + if (centralDirOffset < V2Const.APK_SIG_BLOCK_MIN_SIZE) { + throw new IOException( + "APK too small for APK Signing Block. ZIP Central Directory offset: " + + centralDirOffset); + } + // Read the magic and offset in file from the footer section of the block: + // * uint64: size of block + // * 16 bytes: magic + fileChannel.position(centralDirOffset - 24); + final ByteBuffer footer = ByteBuffer.allocate(24); + fileChannel.read(footer); + footer.order(ByteOrder.LITTLE_ENDIAN); + if ((footer.getLong(8) != V2Const.APK_SIG_BLOCK_MAGIC_LO) + || (footer.getLong(16) != V2Const.APK_SIG_BLOCK_MAGIC_HI)) { + throw new IOException( + "No APK Signing Block before ZIP Central Directory"); + } + // Read and compare size fields + final long apkSigBlockSizeInFooter = footer.getLong(0); + if ((apkSigBlockSizeInFooter < footer.capacity()) + || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { + throw new IOException( + "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); + } + final int totalSize = (int) (apkSigBlockSizeInFooter + 8); + final long apkSigBlockOffset = centralDirOffset - totalSize; + if (apkSigBlockOffset < 0) { + throw new IOException( + "APK Signing Block offset out of range: " + apkSigBlockOffset); + } + fileChannel.position(apkSigBlockOffset); + final ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize); + fileChannel.read(apkSigBlock); + apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); + final long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); + if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { + throw new IOException( + "APK Signing Block sizes in header and footer do not match: " + + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); + } + return Pair.of(apkSigBlock, apkSigBlockOffset); + } + + public static Map findIdValues(final ByteBuffer apkSigningBlock) throws IOException { + checkByteOrderLittleEndian(apkSigningBlock); + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes pairs + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + final ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); + + final Map idValues = new LinkedHashMap(); // keep order + + int entryCount = 0; + while (pairs.hasRemaining()) { + entryCount++; + if (pairs.remaining() < 8) { + throw new IOException( + "Insufficient data to read size of APK Signing Block entry #" + entryCount); + } + final long lenLong = pairs.getLong(); + if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { + throw new IOException( + "APK Signing Block entry #" + entryCount + + " size out of range: " + lenLong); + } + final int len = (int) lenLong; + final int nextEntryPos = pairs.position() + len; + if (len > pairs.remaining()) { + throw new IOException( + "APK Signing Block entry #" + entryCount + " size out of range: " + len + + ", available: " + pairs.remaining()); + } + final int id = pairs.getInt(); + idValues.put(id, getByteBuffer(pairs, len - 4)); + + pairs.position(nextEntryPos); + } + + return idValues; + } + + /** + * Returns new byte buffer whose content is a shared subsequence of this buffer's content + * between the specified start (inclusive) and end (exclusive) positions. As opposed to + * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source + * buffer's byte order. + */ + private static ByteBuffer sliceFromTo(final ByteBuffer source, final int start, final int end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + final int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + final int originalLimit = source.limit(); + final int originalPosition = source.position(); + try { + source.position(0); + source.limit(end); + source.position(start); + final ByteBuffer result = source.slice(); + result.order(source.order()); + return result; + } finally { + source.position(0); + source.limit(originalLimit); + source.position(originalPosition); + } + } + + /** + * Relative readBlock method for reading {@code size} number of bytes from the current + * position of this buffer. + *

+ *

This method reads the next {@code size} bytes at this buffer's current position, + * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to + * {@code size}, byte order set to this buffer's byte order; and then increments the position by + * {@code size}. + */ + private static ByteBuffer getByteBuffer(final ByteBuffer source, final int size) + throws BufferUnderflowException { + if (size < 0) { + throw new IllegalArgumentException("size: " + size); + } + final int originalLimit = source.limit(); + final int position = source.position(); + final int limit = position + size; + if ((limit < position) || (limit > originalLimit)) { + throw new BufferUnderflowException(); + } + source.limit(limit); + try { + final ByteBuffer result = source.slice(); + result.order(source.order()); + source.position(limit); + return result; + } finally { + source.limit(originalLimit); + } + } + + private static void checkByteOrderLittleEndian(final ByteBuffer buffer) { + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + } + + +} diff --git a/common/src/main/java/com/mcxiaoke/packer/support/walle/Pair.java b/common/src/main/java/com/mcxiaoke/packer/support/walle/Pair.java new file mode 100644 index 0000000..b73aae6 --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/support/walle/Pair.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 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.mcxiaoke.packer.support.walle; + +/** + * Pair of two elements. + */ +final class Pair { + private final A f; + private final B s; + + private Pair(final A first, final B second) { + f = first; + s = second; + } + + public static Pair of(final A first, final B second) { + return new Pair(first, second); + } + + public A getFirst() { + return f; + } + + public B getSecond() { + return s; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Pair pair = (Pair) o; + + if (f != null ? !f.equals(pair.f) : pair.f != null) return false; + return s != null ? s.equals(pair.s) : pair.s == null; + } + + @Override + public int hashCode() { + int result = f != null ? f.hashCode() : 0; + result = 31 * result + (s != null ? s.hashCode() : 0); + return result; + } +} diff --git a/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadReader.java b/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadReader.java new file mode 100644 index 0000000..126e2eb --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadReader.java @@ -0,0 +1,49 @@ +package com.mcxiaoke.packer.support.walle; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Map; + +public final class PayloadReader { + private PayloadReader() { + super(); + } + + public static byte[] readBlock(final File apkFile, final int id) + throws IOException { + final ByteBuffer buf = readBlockBuffer(apkFile, id); + return buf == null ? null : V2Utils.getBytes(buf); + } + + public static ByteBuffer readBlockBuffer(final File apkFile, final int id) + throws IOException { + final Map blocks = readAllBlocks(apkFile); + if (blocks == null) { + return null; + } + return blocks.get(id); + } + + private static Map readAllBlocks(final File apkFile) + throws IOException { + Map blocks = null; + + RandomAccessFile raf = null; + FileChannel fc = null; + try { + raf = new RandomAccessFile(apkFile, "r"); + fc = raf.getChannel(); + final ByteBuffer apkSigningBlock = ApkUtil.findApkSigningBlock(fc).getFirst(); + blocks = ApkUtil.findIdValues(apkSigningBlock); + } finally { + V2Utils.close(fc); + V2Utils.close(raf); + } + return blocks; + } + + +} diff --git a/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadWriter.java b/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadWriter.java new file mode 100644 index 0000000..09916d1 --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadWriter.java @@ -0,0 +1,130 @@ +package com.mcxiaoke.packer.support.walle; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + + +public final class PayloadWriter { + private PayloadWriter() { + super(); + } + + public static void writeBlock(File apkFile, final int id, + final byte[] bytes) throws IOException { + final ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.put(bytes, 0, bytes.length); + byteBuffer.flip(); + writeBlock(apkFile, id, byteBuffer); + } + + public static void writeBlock(final File apkFile, final int id, + final ByteBuffer buffer) throws IOException { + final Map idValues = new HashMap<>(); + idValues.put(id, buffer); + writeValues(apkFile, idValues); + } + + /** + * writeBlock new idValues into apk, update if id exists + * NOTE: use unknown IDs. DO NOT use ID that have already been used. See APK Signature Scheme v2 + */ + private static void writeValues(final File apkFile, final Map idValues) throws IOException { + final ApkSigningBlockHandler handler = new ApkSigningBlockHandler() { + @Override + public ApkSigningBlock handle(final Map originIdValues) { + if (idValues != null && !idValues.isEmpty()) { + originIdValues.putAll(idValues); + } + final ApkSigningBlock apkSigningBlock = new ApkSigningBlock(); + final Set> entrySet = originIdValues.entrySet(); + for (Map.Entry entry : entrySet) { + final ApkSigningPayload payload = new ApkSigningPayload(entry.getKey(), entry.getValue()); + apkSigningBlock.addPayload(payload); + } + return apkSigningBlock; + } + }; + writeApkSigningBlock(apkFile, handler); + } + + static void writeApkSigningBlock(final File apkFile, final ApkSigningBlockHandler handler) throws IOException { + RandomAccessFile raf = null; + FileChannel fc = null; + try { + raf = new RandomAccessFile(apkFile, "rw"); + fc = raf.getChannel(); + final long commentLength = ApkUtil.findZipCommentLength(fc); + final long centralDirStartOffset = ApkUtil.findCentralDirStartOffset(fc, commentLength); + // Find the APK Signing Block. The block immediately precedes the Central Directory. + final Pair apkSigningBlockAndOffset = ApkUtil.findApkSigningBlock(fc, centralDirStartOffset); + final ByteBuffer apkSigningBlock2 = apkSigningBlockAndOffset.getFirst(); + final long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond(); + + if (centralDirStartOffset == 0 || apkSigningBlockOffset == 0) { + throw new IOException( + "No APK Signature Scheme v2 block in APK Signing Block"); + } + final Map originIdValues = ApkUtil.findIdValues(apkSigningBlock2); + // Find the APK Signature Scheme v2 Block inside the APK Signing Block. + final ByteBuffer apkSignatureSchemeV2Block = originIdValues.get(V2Const.APK_SIGNATURE_SCHEME_V2_BLOCK_ID); + + if (apkSignatureSchemeV2Block == null) { + throw new IOException( + "No APK Signature Scheme v2 block in APK Signing Block"); + } + final ApkSigningBlock apkSigningBlock = handler.handle(originIdValues); + // read CentralDir + raf.seek(centralDirStartOffset); + final byte[] centralDirBytes = new byte[(int) (fc.size() - centralDirStartOffset)]; + raf.read(centralDirBytes); + + fc.position(apkSigningBlockOffset); + + final long length = apkSigningBlock.writeTo(raf); + + // store CentralDir + raf.write(centralDirBytes); + // update length + raf.setLength(raf.getFilePointer()); + + // update CentralDir Offset + // End of central directory record (EOCD) + // Offset Bytes Description[23] + // 0 4 End of central directory signature = 0x06054b50 + // 4 2 Number of this disk + // 6 2 Disk where central directory starts + // 8 2 Number of central directory records on this disk + // 10 2 Total number of central directory records + // 12 4 Size of central directory (bytes) + // 16 4 Offset of start of central directory, relative to start of archive + // 20 2 Comment length (n) + // 22 n Comment + + raf.seek(fc.size() - commentLength - 6); + // 6 = 2(Comment length) + 4 + // (Offset of start of central directory, relative to start of archive) + final ByteBuffer temp = ByteBuffer.allocate(4); + temp.order(ByteOrder.LITTLE_ENDIAN); + temp.putInt((int) (centralDirStartOffset + length + 8 - (centralDirStartOffset - apkSigningBlockOffset))); + // 8 = size of block in bytes (excluding this field) (uint64) + temp.flip(); + raf.write(temp.array()); + + } finally { + V2Utils.close(fc); + V2Utils.close(raf); + } + } + + interface ApkSigningBlockHandler { + ApkSigningBlock handle(Map originIdValues); + } +} diff --git a/common/src/main/java/com/mcxiaoke/packer/support/walle/V2Const.java b/common/src/main/java/com/mcxiaoke/packer/support/walle/V2Const.java new file mode 100644 index 0000000..d6f26eb --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/support/walle/V2Const.java @@ -0,0 +1,39 @@ +package com.mcxiaoke.packer.support.walle; + +/** + * User: mcxiaoke + * Date: 2017/5/17 + * Time: 15:08 + */ +class V2Const { + // V2 Scheme Constants + public static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; + public static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; + public static final int APK_SIG_BLOCK_MIN_SIZE = 32; + /** + * The v2 signature of the APK is stored as an ID-value pair with ID 0x7109871a + * (https://source.android.com/security/apksigning/v2.html#apk-signing-block) + **/ + public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; + public static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; + /** + * APK Signing Block Magic Code: magic “APK Sig Block 42” (16 bytes) + * "APK Sig Block 42" : 41 50 4B 20 53 69 67 20 42 6C 6F 63 6B 20 34 32 + */ + public static final byte[] APK_SIGNING_BLOCK_MAGIC = + new byte[]{ + 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, + 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, + }; + + // ZIP Constants + public static final int ZIP_EOCD_REC_MIN_SIZE = 22; + public static final int ZIP_EOCD_REC_SIG = 0x06054b50; + public static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10; + public static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; + public static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; + public static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; + public static final int ZIP64_EOCD_LOCATOR_SIZE = 20; + public static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50; + public static final int UINT16_MAX_VALUE = 0xffff; +} diff --git a/common/src/main/java/com/mcxiaoke/packer/support/walle/V2Utils.java b/common/src/main/java/com/mcxiaoke/packer/support/walle/V2Utils.java new file mode 100644 index 0000000..83db7a1 --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/support/walle/V2Utils.java @@ -0,0 +1,29 @@ +package com.mcxiaoke.packer.support.walle; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * User: mcxiaoke + * Date: 2017/5/26 + * Time: 12:10 + */ +final class V2Utils { + + static byte[] getBytes(final ByteBuffer buf) { + final byte[] array = buf.array(); + final int arrayOffset = buf.arrayOffset(); + return Arrays.copyOfRange(array, arrayOffset + buf.position(), + arrayOffset + buf.limit()); + } + + static void close(final Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (IOException ignored) { + } + } +} diff --git a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java new file mode 100644 index 0000000..f06afb6 --- /dev/null +++ b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java @@ -0,0 +1,232 @@ +package com.mcxiaoke.packer.common; + +import com.android.apksig.ApkVerifier; +import com.android.apksig.ApkVerifier.Builder; +import com.android.apksig.ApkVerifier.IssueWithParams; +import com.android.apksig.ApkVerifier.Result; +import com.android.apksig.apk.ApkFormatException; +import com.mcxiaoke.packer.support.walle.PayloadReader; +import com.mcxiaoke.packer.support.walle.PayloadWriter; +import junit.framework.TestCase; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * User: mcxiaoke + * Date: 2017/5/17 + * Time: 16:25 + */ +public class PayloadTests extends TestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + synchronized File newTestFile() throws IOException { + return TestUtils.newTestFile(); + } + + void checkApkVerified(File f) { + try { + assertTrue(TestUtils.apkVerified(f)); + } catch (ApkFormatException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + } + + public void testFileExists() { + File file = new File("data/test.apk"); + assertTrue(file.exists()); + } + + public void testFileCopy() throws IOException { + File f1 = new File("data/test.apk"); + File f2 = newTestFile(); + assertTrue(f2.exists()); + assertTrue(f2.getName().endsWith(".apk")); + assertEquals(f1.length(), f2.length()); + assertEquals(f1.getParent(), f2.getParent()); + } + + public void testFileSignature() throws IOException, + ApkFormatException, + NoSuchAlgorithmException { + File f = newTestFile(); + checkApkVerified(f); + } + + public void testOverrideSignature() throws IOException, + ApkFormatException, + NoSuchAlgorithmException { + File f = newTestFile(); + // don't write with APK Signature Scheme v2 Block ID 0x7109871a + PayloadUtils.writeRaw(f, "OverrideSignatureSchemeBlock", 0x7109871a); + assertEquals("OverrideSignatureSchemeBlock", PayloadUtils.readRaw(f, 0x7109871a)); + ApkVerifier verifier = new Builder(f).build(); + Result result = verifier.verify(); + final List errors = result.getErrors(); + if (errors != null && errors.size() > 0) { + for (IssueWithParams error : errors) { + System.out.println("testOverrideSignature " + error); + } + } + assertTrue(result.containsErrors()); + assertFalse(result.isVerified()); + assertFalse(result.isVerifiedUsingV1Scheme()); + assertFalse(result.isVerifiedUsingV2Scheme()); + } + + public void testBytesWrite1() throws IOException { + File f = newTestFile(); + byte[] in = "Hello".getBytes(); + PayloadWriter.writeBlock(f, 0x12345, in); + byte[] out = PayloadReader.readBlock(f, 0x12345); + assertTrue(TestUtils.sameBytes(in, out)); + checkApkVerified(f); + } + + public void testBytesWrite2() throws IOException { + File f = newTestFile(); + byte[] in = "中文和特殊符号测试!@#¥%……*()《》?:【】、".getBytes("UTF-8"); + PayloadWriter.writeBlock(f, 0x12345, in); + byte[] out = PayloadReader.readBlock(f, 0x12345); + assertTrue(TestUtils.sameBytes(in, out)); + checkApkVerified(f); + } + + public void testStringWrite() throws IOException { + File f = newTestFile(); + PayloadUtils.writeRaw(f, "Test String", 0x717a786b); + assertEquals("Test String", PayloadUtils.readRaw(f, 0x717a786b)); + PayloadUtils.writeRaw(f, "中文和特殊符号测试!@#¥%……*()《》?:【】、", 0x717a786b); + assertEquals("中文和特殊符号测试!@#¥%……*()《》?:【】、", PayloadUtils.readRaw(f, 0x717a786b)); + checkApkVerified(f); + } + + public void testValuesWrite() throws IOException { + File f = newTestFile(); + Map in = new HashMap<>(); + in.put("Channel", "HelloWorld"); + in.put("名字", "哈哈啊哈哈哈"); + in.put("!@#$!%^@&*()_+\"?:><", "渠道Google"); + in.put("12345abcd", "2017"); + PayloadUtils.writeValues(f, in, 0x12345); + Map out = PayloadUtils.readValues(f, 0x12345); + assertNotNull(out); + assertEquals(in.size(), out.size()); + for (Map.Entry entry : in.entrySet()) { + assertEquals(entry.getValue(), out.get(entry.getKey())); + } + checkApkVerified(f); + } + + public void testValuesMixedWrite() throws IOException { + File f = newTestFile(); + Map in = new HashMap<>(); + in.put("!@#$!%^@&*()_+\"?:><", "渠道Google"); + in.put("12345abcd", "2017"); + PayloadUtils.writeValues(f, in, 0x123456); + PayloadUtils.writeChannel(f, "Mixed", "hello", 0x8888); + Map out = PayloadUtils.readValues(f, 0x123456); + assertNotNull(out); + assertEquals(in.size(), out.size()); + for (Map.Entry entry : in.entrySet()) { + assertEquals(entry.getValue(), out.get(entry.getKey())); + } + assertEquals("Mixed", PayloadUtils.readChannel(f, "hello", 0x8888)); + PayloadUtils.writeRaw(f, "RawValue", 0x2017); + assertEquals("RawValue", PayloadUtils.readRaw(f, 0x2017)); + PayloadUtils.writeRaw(f, "OverrideValues", 0x123456); + assertEquals("OverrideValues", PayloadUtils.readRaw(f, 0x123456)); + checkApkVerified(f); + } + + public void testByteBuffer() throws IOException { + byte[] string = "Hello".getBytes(); + ByteBuffer buf = ByteBuffer.allocate(1024); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(123); + buf.putChar('z'); + buf.putShort((short) 2017); + buf.putFloat(3.1415f); + buf.put(string); + buf.putLong(9876543210L); + buf.putDouble(3.14159265); + buf.put((byte) 5); + buf.flip(); // important + TestUtils.showBuffer(buf); + assertEquals(123, buf.getInt()); + assertEquals('z', buf.getChar()); + assertEquals(2017, buf.getShort()); + assertEquals(3.1415f, buf.getFloat()); + byte[] so = new byte[string.length]; + buf.get(so); + assertTrue(TestUtils.sameBytes(string, so)); + assertEquals(9876543210L, buf.getLong()); + assertEquals(3.14159265, buf.getDouble()); + assertEquals((byte) 5, buf.get()); + } + + public void testBufferWrite() throws IOException { + File f = newTestFile(); + byte[] string = "Hello".getBytes(); + ByteBuffer in = ByteBuffer.allocate(1024); + in.order(ByteOrder.LITTLE_ENDIAN); + in.putInt(123); + in.putChar('z'); + in.putShort((short) 2017); + in.putFloat(3.1415f); + in.putLong(9876543210L); + in.putDouble(3.14159265); + in.put((byte) 5); + in.put(string); + in.flip(); // important + TestUtils.showBuffer(in); + PayloadWriter.writeBlock(f, 0x123456, in); + ByteBuffer out = PayloadReader.readBlockBuffer(f, 0x123456); + assertNotNull(out); + TestUtils.showBuffer(out); + assertEquals(123, out.getInt()); + assertEquals('z', out.getChar()); + assertEquals(2017, out.getShort()); + assertEquals(3.1415f, out.getFloat()); + assertEquals(9876543210L, out.getLong()); + assertEquals(3.14159265, out.getDouble()); + assertEquals((byte) 5, out.get()); + byte[] so = new byte[string.length]; + out.get(so); + assertTrue(TestUtils.sameBytes(string, so)); + checkApkVerified(f); + } + + public void testChannelWriteRead() throws IOException { + File f = newTestFile(); + PackerParser p = new PackerParser(f); + p.writeChannel("Hello"); + assertEquals("Hello", p.readChannel()); + p.writeChannel("中文"); + assertEquals("中文", p.readChannel()); + p.writeChannel("中文 C"); + assertEquals("中文 C", p.readChannel()); + checkApkVerified(f); + } + + +} diff --git a/common/src/test/java/com/mcxiaoke/packer/common/TestUtils.java b/common/src/test/java/com/mcxiaoke/packer/common/TestUtils.java new file mode 100644 index 0000000..605e36d --- /dev/null +++ b/common/src/test/java/com/mcxiaoke/packer/common/TestUtils.java @@ -0,0 +1,104 @@ +package com.mcxiaoke.packer.common; + +import com.android.apksig.ApkVerifier; +import com.android.apksig.ApkVerifier.Builder; +import com.android.apksig.ApkVerifier.Result; +import com.android.apksig.apk.ApkFormatException; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +/** + * User: mcxiaoke + * Date: 2017/5/18 + * Time: 16:59 + */ +public class TestUtils { + private final static char[] CHARS = "0123456789ABCDEF".toCharArray(); + + public static boolean sameBytes(byte[] a, byte[] b) { + if (a == null || b == null) { + return false; + } + if (a.length != b.length) { + return false; + } + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + + public static String toHex(ByteBuffer buffer) { + final byte[] array = buffer.array(); + final int arrayOffset = buffer.arrayOffset(); + byte[] data = Arrays.copyOfRange(array, arrayOffset + buffer.position(), + arrayOffset + buffer.limit()); + return toHex(data); + } + + public static String toHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = CHARS[v >>> 4]; + hexChars[j * 2 + 1] = CHARS[v & 0x0F]; + } + return new String(hexChars); + } + + public static File newTestFile() throws IOException { + File dir = new File("data"); + File file = new File(dir, "test.apk"); + File tempfile = new File(dir, System.currentTimeMillis() + "-test.apk"); + FileUtils.copyFile(file, tempfile); + return tempfile; + } + + private static int counter = 0; + + public static void showBuffer(ByteBuffer b) { + StringBuilder s = new StringBuilder(); + s.append("------").append(++counter).append("------\n"); + s.append("capacity=").append(b.capacity()); + s.append(" position=").append(b.position()); + s.append(" limit=").append(b.limit()); + s.append(" remaining=").append(b.remaining()); + s.append(" arrayOffset=").append(b.arrayOffset()); + s.append(" arrayLength=").append(b.array().length).append("\n"); + s.append("array=").append(toHex(b)).append("\n"); + System.out.println(s.toString()); + } + + public static void showBuffer2(final ByteBuffer buffer) { + System.out.println("showBuffer capacity=" + buffer.capacity() + + " position=" + buffer.position() + + " limit=" + buffer.limit() + + " remaining=" + buffer.remaining() + + " arrayOffset=" + buffer.arrayOffset() + + " arrayLength=" + buffer.array().length); +// byte[] all = buffer.array(); +// int offset = buffer.arrayOffset(); +// int start = offset + buffer.position(); +// int end = offset + buffer.limit(); +// byte[] bytes = Arrays.copyOfRange(all, start, end); +// System.out.println(Utils.toHex(bytes)); + } + + public static boolean apkVerified(File f) throws ApkFormatException, + NoSuchAlgorithmException, + IOException { + ApkVerifier verifier = new Builder(f).build(); + Result result = verifier.verify(); + return result.isVerified() + && result.isVerifiedUsingV1Scheme() + && result.isVerifiedUsingV2Scheme() + && !result.containsErrors(); + } +} From a67186b6a64dd357e92fc533e0d21c3d4ac7500b Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 26 May 2017 18:21:50 +0800 Subject: [PATCH 03/67] add new commandline module ,not complete --- cli/build.gradle | 36 ++++ cli/gradle.properties | 3 + .../java/com/mcxiaoke/packer/cli/Main.java | 9 + .../mcxiaoke/packer/cli/OptionsParser.java | 199 ++++++++++++++++++ .../mcxiaoke/packer/cli/PackerNgUtils.java | 48 +++++ cli/src/main/resources/META-INF/MANIFEST.MF | 3 + 6 files changed, 298 insertions(+) create mode 100644 cli/build.gradle create mode 100644 cli/gradle.properties create mode 100644 cli/src/main/java/com/mcxiaoke/packer/cli/Main.java create mode 100644 cli/src/main/java/com/mcxiaoke/packer/cli/OptionsParser.java create mode 100644 cli/src/main/java/com/mcxiaoke/packer/cli/PackerNgUtils.java create mode 100644 cli/src/main/resources/META-INF/MANIFEST.MF diff --git a/cli/build.gradle b/cli/build.gradle new file mode 100644 index 0000000..d9f38be --- /dev/null +++ b/cli/build.gradle @@ -0,0 +1,36 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + } +} + +repositories { + jcenter() +} + +apply plugin: 'java' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +dependencies { + compile project(":common") + compile 'com.android.tools.build:apksig:2.3.0' +} + +task fatJar(type: Jar) { + manifest { + attributes 'Implementation-Title': 'PackerNg 2 Executable Jar', + 'Implementation-Version': VERSION_NAME, + 'Main-Class': 'com.mcxiaoke.packer.cli.Main', + 'Description': 'This is PackerNg 2 executable Jar.' + } + baseName = 'PackerNg' + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + with jar +} + +// apply from: '../jar.gradle' +apply from: '../gradle-mvn-push.gradle' diff --git a/cli/gradle.properties b/cli/gradle.properties new file mode 100644 index 0000000..a4bbb32 --- /dev/null +++ b/cli/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=cli +POM_PACKAGING=jar +POM_NAME=Commandline Classes for Packer-Ng diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java new file mode 100644 index 0000000..908f2b1 --- /dev/null +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java @@ -0,0 +1,9 @@ +package com.mcxiaoke.packer.cli; + +/** + * User: mcxiaoke + * Date: 2017/5/26 + * Time: 15:56 + */ +public class Main { +} diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/OptionsParser.java b/cli/src/main/java/com/mcxiaoke/packer/cli/OptionsParser.java new file mode 100644 index 0000000..ff26d91 --- /dev/null +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/OptionsParser.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2016 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.mcxiaoke.packer.cli; + +import java.util.Arrays; + +/** + * Parser of command-line options/switches/flags. + *

+ *

Supported option formats: + *

    + *
  • {@code --name value}
  • + *
  • {@code --name=value}
  • + *
  • {@code -name value}
  • + *
  • {@code --name} (boolean options only)
  • + *
+ *

+ *

To use the parser, create an instance, providing it with the command-line parameters, then + * iterate over options by invoking {@link #nextOption()} until it returns {@code null}. + */ +class OptionsParser { + private final String[] mParams; + private int mIndex; + private String mLastOptionValue; + private String mLastOptionOriginalForm; + + /** + * Constructs a new {@code OptionsParser} initialized with the provided command-line. + */ + public OptionsParser(String[] params) { + mParams = params.clone(); + } + + /** + * Returns the name (without leading dashes) of the next option (starting with the very first + * option) or {@code null} if there are no options left. + *

+ *

The value of this option can be obtained via {@link #getRequiredValue(String)}, + * {@link #getRequiredIntValue(String)}, and {@link #getOptionalBooleanValue(boolean)}. + */ + public String nextOption() { + if (mIndex >= mParams.length) { + // No more parameters left + return null; + } + String param = mParams[mIndex]; + if (!param.startsWith("-")) { + // Not an option + return null; + } + + mIndex++; + mLastOptionOriginalForm = param; + mLastOptionValue = null; + if (param.startsWith("--")) { + // FORMAT: --name value OR --name=value + if ("--".equals(param)) { + // End of options marker + return null; + } + int valueDelimiterIndex = param.indexOf('='); + if (valueDelimiterIndex != -1) { + mLastOptionValue = param.substring(valueDelimiterIndex + 1); + mLastOptionOriginalForm = param.substring(0, valueDelimiterIndex); + return param.substring("--".length(), valueDelimiterIndex); + } else { + return param.substring("--".length()); + } + } else { + // FORMAT: -name value + return param.substring("-".length()); + } + } + + /** + * Returns the original form of the current option. The original form includes the leading dash + * or dashes. This is intended to be used for referencing the option in error messages. + */ + public String getOptionOriginalForm() { + return mLastOptionOriginalForm; + } + + /** + * Returns the value of the current option, throwing an exception if the value is missing. + */ + public String getRequiredValue(String valueDescription) throws OptionsException { + if (mLastOptionValue != null) { + String result = mLastOptionValue; + mLastOptionValue = null; + return result; + } + if (mIndex >= mParams.length) { + // No more parameters left + throw new OptionsException( + valueDescription + " missing after " + mLastOptionOriginalForm); + } + String param = mParams[mIndex]; + if ("--".equals(param)) { + // End of options marker + throw new OptionsException( + valueDescription + " missing after " + mLastOptionOriginalForm); + } + mIndex++; + return param; + } + + /** + * Returns the value of the current numeric option, throwing an exception if the value is + * missing or is not numeric. + */ + public int getRequiredIntValue(String valueDescription) throws OptionsException { + String value = getRequiredValue(valueDescription); + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new OptionsException( + valueDescription + " (" + mLastOptionOriginalForm + + ") must be a decimal number: " + value); + } + } + + /** + * Gets the value of the current boolean option. Boolean options are not required to have + * explicitly specified values. + */ + public boolean getOptionalBooleanValue(boolean defaultValue) throws OptionsException { + if (mLastOptionValue != null) { + // --option=value form + String stringValue = mLastOptionValue; + mLastOptionValue = null; + if ("true".equals(stringValue)) { + return true; + } else if ("false".equals(stringValue)) { + return false; + } + throw new OptionsException( + "Unsupported value for " + mLastOptionOriginalForm + ": " + stringValue + + ". Only true or false supported."); + } + + // --option (true|false) form OR just --option + if (mIndex >= mParams.length) { + return defaultValue; + } + + String stringValue = mParams[mIndex]; + if ("true".equals(stringValue)) { + mIndex++; + return true; + } else if ("false".equals(stringValue)) { + mIndex++; + return false; + } else { + return defaultValue; + } + } + + /** + * Returns the remaining command-line parameters. This is intended to be invoked once + * {@link #nextOption()} returns {@code null}. + */ + public String[] getRemainingParams() { + if (mIndex >= mParams.length) { + return new String[0]; + } + String param = mParams[mIndex]; + if ("--".equals(param)) { + // Skip end of options marker + return Arrays.copyOfRange(mParams, mIndex + 1, mParams.length); + } else { + return Arrays.copyOfRange(mParams, mIndex, mParams.length); + } + } + + /** + * Indicates that an error was encountered while parsing command-line options. + */ + public static class OptionsException extends Exception { + private static final long serialVersionUID = 1L; + + public OptionsException(String message) { + super(message); + } + } +} diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/PackerNgUtils.java b/cli/src/main/java/com/mcxiaoke/packer/cli/PackerNgUtils.java new file mode 100644 index 0000000..c5a0d40 --- /dev/null +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/PackerNgUtils.java @@ -0,0 +1,48 @@ +package com.mcxiaoke.packer.cli; + +import com.android.apksig.ApkVerifier; +import com.android.apksig.ApkVerifier.Builder; +import com.android.apksig.ApkVerifier.Result; +import com.android.apksig.apk.ApkFormatException; +import com.mcxiaoke.packer.common.PackerParser; + +import java.io.File; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + +/** + * User: mcxiaoke + * Date: 2017/5/26 + * Time: 16:21 + */ +public class PackerNgUtils { + + public static void writeChannel(File apkFile, String channel) throws IOException { + PackerParser.create(apkFile).writeChannel(channel); + } + + public static String readChannel(File apkFile) throws IOException { + return PackerParser.create(apkFile).readChannel(); + } + + public static boolean verifyChannel(File apkFile, String channel) throws IOException { + return verifyApk(apkFile) && (channel.equals(readChannel(apkFile))); + } + + public static boolean verifyApk(File f) throws IOException { + ApkVerifier verifier = new Builder(f).build(); + try { + Result result = verifier.verify(); + return result.isVerified() + && result.isVerifiedUsingV1Scheme() + && result.isVerifiedUsingV2Scheme() + && !result.containsErrors(); + } catch (ApkFormatException e) { + throw new IOException(e); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e); + } + + } + +} diff --git a/cli/src/main/resources/META-INF/MANIFEST.MF b/cli/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..077b411 --- /dev/null +++ b/cli/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: com.mcxiaoke.packer.helper.PackerNg2 + From 2354330906291ae0acf443320781662d976eac14 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 26 May 2017 18:22:20 +0800 Subject: [PATCH 04/67] using new v2 signature common module --- plugin/build.gradle | 3 ++- .../packer/ng/ArchiveAllApkTask.groovy | 24 +++++-------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/plugin/build.gradle b/plugin/build.gradle index 253d1e4..f4645f4 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -16,7 +16,8 @@ repositories { dependencies { compile localGroovy() compile gradleApi() - compile project(':helper') + compile project(':common') + compile project(':cli') compile 'com.android.tools.build:gradle:2.2.2' compile 'com.android.tools.build:apksig:2.3.0' } diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy index 14362df..6db0a54 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy @@ -2,7 +2,7 @@ package com.mcxiaoke.packer.ng import com.android.build.gradle.api.BaseVariant import com.android.builder.model.SigningConfig -import com.mcxiaoke.packer.helper.PackerNg +import com.mcxiaoke.packer.cli.PackerNgUtils import groovy.io.FileType import groovy.text.SimpleTemplateEngine import org.gradle.api.DefaultTask @@ -12,8 +12,6 @@ import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction import java.text.SimpleDateFormat -import java.util.jar.JarEntry -import java.util.jar.JarFile /** * User: mcxiaoke @@ -66,24 +64,14 @@ class ArchiveAllApkTask extends DefaultTask { "please check your signingConfig!") } - // ensure APK Signature Scheme v2 disabled. - if (signingConfig.hasProperty("v2SigningEnabled") && - signingConfig.v2SigningEnabled == true) { - throw new GradleException("Please add 'v2SigningEnabled false' " + - "to signingConfig to disable APK Signature Scheme v2, " + - "as it's not compatible with packer-ng plugin, more details at " + - "https://github.com/mcxiaoke/packer-ng-plugin/blob/master/compatibility.md.") - } } void checkApkSignature(File file) throws GradleException { File apkPath = project.rootDir.toPath().relativize(file.toPath()).toFile() - JarFile jarFile = new JarFile(file) - JarEntry mfEntry = jarFile.getJarEntry("META-INF/MANIFEST.MF") - JarEntry certEntry = jarFile.getJarEntry("META-INF/CERT.SF") - if (mfEntry == null || certEntry == null) { + boolean apkVerified = PackerNgUtils.verifyApk(apkPath) + if (!apkVerified) { throw new GradleException(":${name} " + - "apk ${apkPath} not signed, please check your signingConfig!") + "apk ${apkPath} not v2 signed, please check your signingConfig!") } } @@ -114,10 +102,10 @@ class ArchiveAllApkTask extends DefaultTask { File tempFile = new File(outputDir, market + ".tmp") copyTo(originalFile, tempFile) try { - PackerNg.Helper.writeMarket(tempFile, market) + PackerNgUtils.writeChannel(tempFile, market) String apkName = buildApkName(theVariant, market, tempFile) File finalFile = new File(outputDir, apkName) - if (PackerNg.Helper.verifyMarket(tempFile, market)) { + if (PackerNgUtils.verifyChannel(tempFile, market)) { println(":${project.name}:${name} Generating apk for ${market}") tempFile.renameTo(finalFile) } else { From 3e6a575ccd9f93a6e7d64fb851d0ad88b5f4726b Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 26 May 2017 18:22:45 +0800 Subject: [PATCH 05/67] new helper android library module --- helper/build.gradle | 39 +- helper/src/main/AndroidManifest.xml | 4 + .../com/mcxiaoke/packer/helper/PackerNg.java | 452 ++---------------- 3 files changed, 60 insertions(+), 435 deletions(-) create mode 100644 helper/src/main/AndroidManifest.xml diff --git a/helper/build.gradle b/helper/build.gradle index 09c2de7..cddbfbd 100644 --- a/helper/build.gradle +++ b/helper/build.gradle @@ -3,6 +3,7 @@ buildscript { jcenter() } dependencies { + classpath "com.android.tools.build:gradle:2.2.2" } } @@ -10,25 +11,33 @@ repositories { jcenter() } -apply plugin: 'java' - -sourceCompatibility = 1.7 -targetCompatibility = 1.7 +apply plugin: 'com.android.library' dependencies { + compile project(":common") } -task fatJar(type: Jar) { - manifest { - attributes 'Implementation-Title': 'PackerNg Executable Jar', - 'Implementation-Version': VERSION_NAME, - 'Main-Class': 'com.mcxiaoke.packer.helper.PackerNg', - 'Description': 'This is PackerNg executable Jar.' +android { + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + encoding "UTF-8" } - baseName = 'PackerNg' - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } - with jar -} -// apply from: '../jar.gradle' + compileSdkVersion project.compileSdkVersion + buildToolsVersion project.buildToolsVersion + + defaultConfig { + versionName project.VERSION_NAME + versionCode Integer.parseInt(project.VERSION_CODE) + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion + } + + lintOptions { + abortOnError false + htmlReport true + } +} apply from: '../gradle-mvn-push.gradle' diff --git a/helper/src/main/AndroidManifest.xml b/helper/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2a2c657 --- /dev/null +++ b/helper/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java index 11383a7..817833e 100644 --- a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java +++ b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java @@ -1,24 +1,10 @@ package com.mcxiaoke.packer.helper; -import java.io.BufferedReader; -import java.io.DataInput; -import java.io.DataOutput; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import com.mcxiaoke.packer.common.PackerParser; + import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileChannel.MapMode; -import java.util.ArrayList; -import java.util.List; /** * User: mcxiaoke @@ -28,433 +14,59 @@ public final class PackerNg { private static final String TAG = PackerNg.class.getSimpleName(); private static final String EMPTY_STRING = ""; - private static String sCachedMarket; + private static String sCachedChannel; - public static String getMarket(final Object context) { - return getMarket(context, EMPTY_STRING); + public static String getChannel(final Context context) { + return getChannel(context, EMPTY_STRING); } - public static synchronized String getMarket(final Object context, final String defaultValue) { - if (sCachedMarket == null) { - sCachedMarket = getMarketInternal(context, defaultValue).market; + public static synchronized String getChannel(final Context context, + final String defValue) { + if (sCachedChannel == null) { + sCachedChannel = getMarketInternal(context, defValue).channel; } - return sCachedMarket; + return sCachedChannel; } - public static MarketInfo getMarketInfo(final Object context) { - return getMarketInfo(context, EMPTY_STRING); + public static ChannelInfo getChannelInfo(final Context context) { + return getChannelInfo(context, EMPTY_STRING); } - public static synchronized MarketInfo getMarketInfo(final Object context, final String defaultValue) { - return getMarketInternal(context, defaultValue); + public static synchronized ChannelInfo getChannelInfo(final Context context, + final String defValue) { + return getMarketInternal(context, defValue); } - private static MarketInfo getMarketInternal(final Object context, final String defaultValue) { - String market; - Exception error; + private static ChannelInfo getMarketInternal(final Context context, + final String defValue) { + String market = null; + Exception error = null; try { - final String sourceDir = Helper.getSourceDir(context); - market = Helper.readMarket(new File(sourceDir)); - error = null; + final ApplicationInfo info = context.getApplicationInfo(); + final File apkFile = new File(info.sourceDir); + final PackerParser parser = PackerParser.create(apkFile); + market = parser.readChannel(); } catch (Exception e) { - market = null; error = e; } - return new MarketInfo(market == null ? defaultValue : market, error); + return new ChannelInfo(market == null ? defValue : market, error); } - public static final class MarketInfo { - public final String market; + public static final class ChannelInfo { + public final String channel; public final Exception error; - public MarketInfo(final String market, final Exception error) { - this.market = market; + public ChannelInfo(final String channel, final Exception error) { + this.channel = channel; this.error = error; } @Override public String toString() { - return "MarketInfo{" + - "market='" + market + '\'' + + return "ChannelInfo{" + + "market='" + channel + '\'' + ", error=" + error + '}'; } } - - public static class MarketExistsException extends IOException { - public MarketExistsException() { - super(); - } - - public MarketExistsException(final String message) { - super(message); - } - } - - public static class MarketNotFoundException extends IOException { - public MarketNotFoundException() { - super(); - } - - public MarketNotFoundException(final String message) { - super(message); - } - } - - public static class Helper { - static final String UTF_8 = "UTF-8"; - static final int ZIP_COMMENT_MAX_LENGTH = 65535; - static final int SHORT_LENGTH = 2; - static final byte[] MAGIC = new byte[]{0x21, 0x5a, 0x58, 0x4b, 0x21}; //!ZXK! - - // for android code - private static String getSourceDir(final Object context) - throws ClassNotFoundException, - InvocationTargetException, - IllegalAccessException, - NoSuchFieldException, - NoSuchMethodException { - final Class contextClass = Class.forName("android.content.Context"); - final Class applicationInfoClass = Class.forName("android.content.pm.ApplicationInfo"); - final Method getApplicationInfoMethod = contextClass.getMethod("getApplicationInfo"); - final Object appInfo = getApplicationInfoMethod.invoke(context); - // try ApplicationInfo.sourceDir - Field sourceDirField = applicationInfoClass.getField("sourceDir"); - String sourceDir = (String) sourceDirField.get(appInfo); - if (sourceDir == null) { - // try ApplicationInfo.publicSourceDir - sourceDirField = applicationInfoClass.getField("publicSourceDir"); - sourceDir = (String) sourceDirField.get(appInfo); - } - if (sourceDir == null) { - // try Context.getPackageCodePath() - final Method getPackageCodePathMethod = contextClass.getMethod("getPackageCodePath"); - sourceDir = (String) getPackageCodePathMethod.invoke(context); - } - return sourceDir; - - } - - private static boolean isMagicMatched(byte[] buffer) { - if (buffer.length != MAGIC.length) { - return false; - } - for (int i = 0; i < MAGIC.length; ++i) { - if (buffer[i] != MAGIC[i]) { - return false; - } - } - return true; - } - - private static void writeBytes(byte[] data, DataOutput out) throws IOException { - out.write(data); - } - - private static void writeShort(int i, DataOutput out) throws IOException { - ByteBuffer bb = ByteBuffer.allocate(SHORT_LENGTH).order(ByteOrder.LITTLE_ENDIAN); - bb.putShort((short) i); - out.write(bb.array()); - } - - private static short readShort(DataInput input) throws IOException { - byte[] buf = new byte[SHORT_LENGTH]; - input.readFully(buf); - ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN); - return bb.getShort(0); - } - - - public static void writeZipComment(File file, String comment) throws IOException { - if (hasZipCommentMagic(file)) { - throw new MarketExistsException("Zip comment already exists, ignore."); - } - // {@see java.util.zip.ZipOutputStream.writeEND} - byte[] data = comment.getBytes(UTF_8); - final RandomAccessFile raf = new RandomAccessFile(file, "rw"); - raf.seek(file.length() - SHORT_LENGTH); - // write zip comment length - // (content field length + length field length + magic field length) - writeShort(data.length + SHORT_LENGTH + MAGIC.length, raf); - // write content - writeBytes(data, raf); - // write content length - writeShort(data.length, raf); - // write magic bytes - writeBytes(MAGIC, raf); - raf.close(); - } - - public static boolean hasZipCommentMagic(File file) throws IOException { - RandomAccessFile raf = null; - try { - raf = new RandomAccessFile(file, "r"); - long index = raf.length(); - byte[] buffer = new byte[MAGIC.length]; - index -= MAGIC.length; - // read magic bytes - raf.seek(index); - raf.readFully(buffer); - // check magic bytes matched - return isMagicMatched(buffer); - } finally { - if (raf != null) { - raf.close(); - } - } - } - - public static String readZipComment(File file) throws IOException { - RandomAccessFile raf = null; - try { - raf = new RandomAccessFile(file, "r"); - long index = raf.length(); - byte[] buffer = new byte[MAGIC.length]; - index -= MAGIC.length; - // read magic bytes - raf.seek(index); - raf.readFully(buffer); - // if magic bytes matched - if (isMagicMatched(buffer)) { - index -= SHORT_LENGTH; - raf.seek(index); - // read content length field - int length = readShort(raf); - if (length > 0) { - index -= length; - raf.seek(index); - // read content bytes - byte[] bytesComment = new byte[length]; - raf.readFully(bytesComment); - return new String(bytesComment, UTF_8); - } else { - throw new MarketNotFoundException("Zip comment content not found"); - } - } else { - throw new MarketNotFoundException("Zip comment magic bytes not found"); - } - } finally { - if (raf != null) { - raf.close(); - } - } - } - - private static String readZipCommentMmp(File file) throws IOException { - final int mappedSize = 10240; - final long fz = file.length(); - RandomAccessFile raf = null; - MappedByteBuffer map = null; - try { - raf = new RandomAccessFile(file, "r"); - map = raf.getChannel().map(MapMode.READ_ONLY, fz - mappedSize, mappedSize); - map.order(ByteOrder.LITTLE_ENDIAN); - int index = mappedSize; - byte[] buffer = new byte[MAGIC.length]; - index -= MAGIC.length; - // read magic bytes - map.position(index); - map.get(buffer); - // if magic bytes matched - if (isMagicMatched(buffer)) { - index -= SHORT_LENGTH; - map.position(index); - // read content length field - int length = map.getShort(); - if (length > 0) { - index -= length; - map.position(index); - // read content bytes - byte[] bytesComment = new byte[length]; - map.get(bytesComment); - return new String(bytesComment, UTF_8); - } - } - } finally { - if (map != null) { - map.clear(); - } - if (raf != null) { - raf.close(); - } - } - return null; - } - - - public static void writeMarket(final File file, final String market) throws IOException { - writeZipComment(file, market); - } - - public static String readMarket(final File file) throws IOException { - return readZipComment(file); - } - - public static boolean verifyMarket(final File file, final String market) throws IOException { - return market.equals(readMarket(file)); - } - - public static void println(String msg) { - System.out.println(msg); - } - - public static void printErr(String msg) { - System.err.println(msg); - } - - public static List parseMarkets(final File file) throws IOException { - final List markets = new ArrayList(); - FileReader fr = new FileReader(file); - BufferedReader br = new BufferedReader(fr); - String line = null; - int lineNo = 1; - while ((line = br.readLine()) != null) { - String parts[] = line.split("#"); - if (parts.length > 0) { - final String market = parts[0].trim(); - if (market.length() > 0) { - markets.add(market); - } - } - ++lineNo; - } - br.close(); - fr.close(); - return markets; - } - - public static void copyFile(File src, File dest) throws IOException { - if (!dest.exists()) { - dest.createNewFile(); - } - FileChannel source = null; - FileChannel destination = null; - try { - source = new FileInputStream(src).getChannel(); - destination = new FileOutputStream(dest).getChannel(); - destination.transferFrom(source, 0, source.size()); - } finally { - if (source != null) { - source.close(); - } - if (destination != null) { - destination.close(); - } - } - } - - public static boolean deleteDir(File dir) { - File[] files = dir.listFiles(); - if (files == null || files.length == 0) { - return false; - } - for (File file : files) { - if (file.isDirectory()) { - deleteDir(file); - } else { - file.delete(); - } - } - return true; - } - - public static String getExtension(final String fileName) { - int dot = fileName.lastIndexOf("."); - if (dot > 0) { - return fileName.substring(dot + 1); - } else { - return null; - } - } - - public static String getBaseName(final String fileName) { - int dot = fileName.lastIndexOf("."); - if (dot > 0) { - return fileName.substring(0, dot); - } else { - return fileName; - } - } - } - - private static final String USAGE_TEXT = - "Usage: java -jar PackerNg-x.x.x.jar apkFile marketFile [outputDir] "; - private static final String INTRO_TEXT = - "\nAttention: if your app using Android gradle plugin 2.2.0 or later, " + - "be sure to install one of the generated Apks to device or emulator, " + - "to ensure the apk can be installed without errors. " + - "More details please go to github " + - "https://github.com/mcxiaoke/packer-ng-plugin .\n"; - - public static void main(String[] args) { - if (args.length < 2) { - Helper.println(USAGE_TEXT); - Helper.println(INTRO_TEXT); - System.exit(1); - } - File apkFile = new File(args[0]); - File marketFile = new File(args[1]); - File outputDir = new File(args.length >= 3 ? args[2] : "apks"); - if (!apkFile.exists()) { - Helper.printErr("Apk file '" + apkFile.getAbsolutePath() + - "' is not exists or not readable."); - Helper.println(USAGE_TEXT); - System.exit(1); - return; - } - if (!marketFile.exists()) { - Helper.printErr("Market file '" + marketFile.getAbsolutePath() + - "' is not exists or not readable."); - Helper.println(USAGE_TEXT); - System.exit(1); - return; - } - if (!outputDir.exists()) { - outputDir.mkdirs(); - } - Helper.println("Apk File: " + apkFile.getAbsolutePath()); - Helper.println("Market File: " + marketFile.getAbsolutePath()); - Helper.println("Output Dir: " + outputDir.getAbsolutePath()); - List markets = null; - try { - markets = Helper.parseMarkets(marketFile); - } catch (IOException e) { - Helper.printErr("Market file parse failed."); - System.exit(1); - } - if (markets == null || markets.isEmpty()) { - Helper.printErr("No markets found."); - System.exit(1); - return; - } - final String baseName = Helper.getBaseName(apkFile.getName()); - final String extName = Helper.getExtension(apkFile.getName()); - int processed = 0; - try { - for (final String market : markets) { - final String apkName = baseName + "-" + market + "." + extName; - File destFile = new File(outputDir, apkName); - Helper.copyFile(apkFile, destFile); - Helper.writeMarket(destFile, market); - if (Helper.verifyMarket(destFile, market)) { - ++processed; - Helper.println("Generating apk " + apkName); - } else { - destFile.delete(); - Helper.printErr("Failed to generate " + apkName); - } - } - Helper.println("[Success] All " + processed - + " apks saved to " + outputDir.getAbsolutePath()); - Helper.println(INTRO_TEXT); - } catch (MarketExistsException ex) { - Helper.printErr("Market info exists in '" + apkFile - + "', please using a clean apk."); - System.exit(1); - } catch (IOException ex) { - Helper.printErr("" + ex); - System.exit(1); - } - } - } From bcfe56061af77491eee25a6895bee8442bb7eafd Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 26 May 2017 18:23:03 +0800 Subject: [PATCH 06/67] udpate samples, support v2 signature --- sample/build.gradle | 35 ++++++++++++------- sample/gradle.properties | 5 +-- sample/src/main/AndroidManifest.xml | 6 ++-- .../mcxiaoke/packer/samples/MainActivity.java | 8 ++--- .../com/mcxiaoke/packer/samples/ResUtils.java | 20 ----------- 5 files changed, 30 insertions(+), 44 deletions(-) diff --git a/sample/build.gradle b/sample/build.gradle index f49df1e..c25c287 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,22 +1,22 @@ buildscript { - ext.packer_version = '1.0.9' + ext.packer_version = '1.0.1-SNAPSHOT' repositories { maven { url '/tmp/repo/' } jcenter() -// maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } dependencies { classpath "com.android.tools.build:gradle:2.2.2" - classpath "com.mcxiaoke.gradle:packer-ng:$packer_version" + classpath "com.mcxiaoke.packer-ng:plugin:$packer_version" } } repositories { maven { url '/tmp/repo/' } jcenter() -// maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } apply plugin: 'com.android.application' @@ -25,7 +25,7 @@ apply plugin: 'packer' // https://code.google.com/p/android/issues/detail?id=171089 dependencies { // compile project(':helper') - compile "com.mcxiaoke.gradle:packer-helper:$packer_version" + compile "com.mcxiaoke.packer-ng:helper:$packer_version" compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:support-v4:25.3.1' compile 'com.android.support:appcompat-v7:25.3.1' @@ -36,8 +36,8 @@ dependencies { } packer { - checkSigningConfig = true - checkZipAlign = true + checkSigningConfig = false + checkZipAlign = false archiveNameFormat = '${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}-${fileMD5}' archiveOutput = file(new File(project.rootProject.buildDir.path, "myapks")) } @@ -49,15 +49,15 @@ android { targetCompatibility JavaVersion.VERSION_1_7 encoding "UTF-8" } - - compileSdkVersion Integer.parseInt(project.COMPILE_SDK_VERSION) - buildToolsVersion project.BUILD_TOOLS_VERSION + + compileSdkVersion project.compileSdkVersion + buildToolsVersion project.buildToolsVersion defaultConfig { versionName project.VERSION_NAME versionCode Integer.parseInt(project.VERSION_CODE) - minSdkVersion Integer.parseInt(project.MIN_SDK_VERSION) - targetSdkVersion Integer.parseInt(project.TARGET_SDK_VERSION) + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion } signingConfigs { @@ -66,7 +66,15 @@ android { storePassword "android" keyAlias "android" keyPassword "android" - v2SigningEnabled false + v2SigningEnabled true + } + + v2dev { + storeFile file("android.keystore") + storePassword "android" + keyAlias "android" + keyPassword "android" + v2SigningEnabled true } } @@ -110,6 +118,7 @@ android { packagingOptions { exclude 'LICENSE.txt' + exclude 'META-INF/services/javax.annotation.processing.Processor' } } diff --git a/sample/gradle.properties b/sample/gradle.properties index 26bd099..8b13789 100644 --- a/sample/gradle.properties +++ b/sample/gradle.properties @@ -1,4 +1 @@ -MIN_SDK_VERSION=15 -TARGET_SDK_VERSION=25 -COMPILE_SDK_VERSION=25 -BUILD_TOOLS_VERSION=25.0.3 + diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index e0daecf..626aa41 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + package="com.mcxiaoke.packer.samples"> @@ -9,13 +9,13 @@ diff --git a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java index 1ec9760..2acefe2 100644 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java +++ b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java @@ -25,8 +25,8 @@ import com.mcxiaoke.next.utils.LogUtils; import com.mcxiaoke.next.utils.StringUtils; import com.mcxiaoke.packer.helper.PackerNg; -import com.mcxiaoke.packer.ng.sample.BuildConfig; -import com.mcxiaoke.packer.ng.sample.R; +import com.mcxiaoke.packer.samples.BuildConfig; +import com.mcxiaoke.packer.samples.R; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -64,8 +64,8 @@ private void addAppInfoSection() { StringBuilder builder = new StringBuilder(); builder.append("[AppInfo]\n"); builder.append("SourceDir: ").append(getSourceDir(this)).append("\n"); - builder.append("Market: ").append(PackerNg.getMarket(this)).append("\n"); - builder.append("MarketInfo: ").append(PackerNg.getMarketInfo(this)).append("\n"); + builder.append("Market: ").append(PackerNg.getChannel(this)).append("\n"); + builder.append("MarketInfo: ").append(PackerNg.getChannel(this)).append("\n"); builder.append("Name: ").append(getString(info.labelRes)).append("\n"); builder.append("Package: ").append(BuildConfig.APPLICATION_ID).append("\n"); builder.append("VersionCode: ").append(BuildConfig.VERSION_CODE).append("\n"); diff --git a/sample/src/main/java/com/mcxiaoke/packer/samples/ResUtils.java b/sample/src/main/java/com/mcxiaoke/packer/samples/ResUtils.java index 7423ebc..9d37104 100644 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/ResUtils.java +++ b/sample/src/main/java/com/mcxiaoke/packer/samples/ResUtils.java @@ -1,7 +1,6 @@ package com.mcxiaoke.packer.samples; import android.content.Context; -import com.mcxiaoke.packer.helper.PackerNg; /** * User: mcxiaoke @@ -10,25 +9,6 @@ */ public class ResUtils { - - // for icon R.drawable.ic_search for market Google - // you should name it ic_search_Google.png - public static int getMarketDrawableId(Context context, String resName) { - return getDrawableResId(context, resName + "_" + PackerNg.getMarket(context).toLowerCase()); - } - - public static int getMarketLayoutId(Context context, String resName) { - return getLayoutResId(context, resName + "_" + PackerNg.getMarket(context).toLowerCase()); - } - - public static int getMarketStringId(Context context, String resName) { - return getStringResId(context, resName + "_" + PackerNg.getMarket(context).toLowerCase()); - } - - public static int getMarketResourceId(Context context, String resName) { - return getResourceId(context, resName + "_" + PackerNg.getMarket(context).toLowerCase()); - } - public static int getDrawableResId(Context context, String resName) { return getResId(context, "drawable", resName); } From 20c1e3c639f17ddeb4bb1ce954d753fd02894bfc Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 26 May 2017 18:23:26 +0800 Subject: [PATCH 07/67] update gradle and dependencies --- build.gradle | 9 +++++++++ gradle.properties | 8 ++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- settings.gradle | 1 + 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index e69de29..bf928f0 100644 --- a/build.gradle +++ b/build.gradle @@ -0,0 +1,9 @@ + +ext { + compileSdkVersion = 25 + buildToolsVersion = "25.0.3" + minSdkVersion = 14 + targetSdkVersion = 22 + versionName = "1.1.0-SNAPSHIT" + versionCode = 110 +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 25ea85a..1230446 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,9 @@ -VERSION_NAME=1.1.0-SNAPSHIT +VERSION_NAME=1.0.1-SNAPSHOT VERSION_CODE=110 -GROUP=com.mcxiaoke.packerng +GROUP=com.mcxiaoke.packer-ng -POM_DESCRIPTION=Next Generation Android Multi Market Packer Gradle Plugin +POM_DESCRIPTION=Next Generation Android Multi Packer Gradle Plugin POM_URL=https://github.com/mcxiaoke/packer-ng-plugin POM_SCM_URL=https://github.com/mcxiaoke/packer-ng-plugin.git POM_SCM_CONNECTION=scm:git:https://github.com/mcxiaoke/packer-ng-plugin.git @@ -13,4 +13,4 @@ POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0 POM_LICENCE_DIST=repo POM_DEVELOPER_ID=mcxiaoke POM_DEVELOPER_NAME=Xiaoke Zhang -POM_DEVELOPER_EMAIL=packer-ng-plugin@mcxiaoke.com +POM_DEVELOPER_EMAIL=packer-ng@mcxiaoke.com diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4bb29a4..701468c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-bin.zip diff --git a/gradlew b/gradlew index 91a7e26..cc60c88 100755 --- a/gradlew +++ b/gradlew @@ -127,7 +127,7 @@ if $cygwin ; then if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh + # Now handle the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` diff --git a/settings.gradle b/settings.gradle index 70ea61b..0ab5d7e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ include ':common' include ':plugin' +include ':cli' include ':helper' include ':sample' From 6ecc0ed84dabf333bd85bb8cf3479295021a1110 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 31 May 2017 13:55:36 +0800 Subject: [PATCH 08/67] rename to Operator class --- .../packer/cli/{PackerNgUtils.java => Operator.java} | 6 +++--- .../com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) rename cli/src/main/java/com/mcxiaoke/packer/cli/{PackerNgUtils.java => Operator.java} (89%) diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/PackerNgUtils.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Operator.java similarity index 89% rename from cli/src/main/java/com/mcxiaoke/packer/cli/PackerNgUtils.java rename to cli/src/main/java/com/mcxiaoke/packer/cli/Operator.java index c5a0d40..f5e2f67 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/PackerNgUtils.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Operator.java @@ -15,7 +15,7 @@ * Date: 2017/5/26 * Time: 16:21 */ -public class PackerNgUtils { +public class Operator { public static void writeChannel(File apkFile, String channel) throws IOException { PackerParser.create(apkFile).writeChannel(channel); @@ -29,8 +29,8 @@ public static boolean verifyChannel(File apkFile, String channel) throws IOExcep return verifyApk(apkFile) && (channel.equals(readChannel(apkFile))); } - public static boolean verifyApk(File f) throws IOException { - ApkVerifier verifier = new Builder(f).build(); + public static boolean verifyApk(File apkFile) throws IOException { + ApkVerifier verifier = new Builder(apkFile).build(); try { Result result = verifier.verify(); return result.isVerified() diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy index 6db0a54..7238722 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy @@ -2,7 +2,7 @@ package com.mcxiaoke.packer.ng import com.android.build.gradle.api.BaseVariant import com.android.builder.model.SigningConfig -import com.mcxiaoke.packer.cli.PackerNgUtils +import com.mcxiaoke.packer.cli.Operator import groovy.io.FileType import groovy.text.SimpleTemplateEngine import org.gradle.api.DefaultTask @@ -68,7 +68,7 @@ class ArchiveAllApkTask extends DefaultTask { void checkApkSignature(File file) throws GradleException { File apkPath = project.rootDir.toPath().relativize(file.toPath()).toFile() - boolean apkVerified = PackerNgUtils.verifyApk(apkPath) + boolean apkVerified = Operator.verifyApk(apkPath) if (!apkVerified) { throw new GradleException(":${name} " + "apk ${apkPath} not v2 signed, please check your signingConfig!") @@ -102,10 +102,10 @@ class ArchiveAllApkTask extends DefaultTask { File tempFile = new File(outputDir, market + ".tmp") copyTo(originalFile, tempFile) try { - PackerNgUtils.writeChannel(tempFile, market) + Operator.writeChannel(tempFile, market) String apkName = buildApkName(theVariant, market, tempFile) File finalFile = new File(outputDir, apkName) - if (PackerNgUtils.verifyChannel(tempFile, market)) { + if (Operator.verifyChannel(tempFile, market)) { println(":${project.name}:${name} Generating apk for ${market}") tempFile.renameTo(finalFile) } else { From 69499c77a86b367ac8c121e1f926b422bd9239cd Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 31 May 2017 15:33:42 +0800 Subject: [PATCH 09/67] cli: rename to Options class --- .../cli/{OptionsParser.java => Options.java} | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) rename cli/src/main/java/com/mcxiaoke/packer/cli/{OptionsParser.java => Options.java} (75%) diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/OptionsParser.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Options.java similarity index 75% rename from cli/src/main/java/com/mcxiaoke/packer/cli/OptionsParser.java rename to cli/src/main/java/com/mcxiaoke/packer/cli/Options.java index ff26d91..df299fc 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/OptionsParser.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Options.java @@ -32,17 +32,17 @@ *

To use the parser, create an instance, providing it with the command-line parameters, then * iterate over options by invoking {@link #nextOption()} until it returns {@code null}. */ -class OptionsParser { - private final String[] mParams; - private int mIndex; - private String mLastOptionValue; - private String mLastOptionOriginalForm; +class Options { + private final String[] params; + private int index; + private String lastOptionValue; + private String lastOptionOriginalForm; /** * Constructs a new {@code OptionsParser} initialized with the provided command-line. */ - public OptionsParser(String[] params) { - mParams = params.clone(); + public Options(String[] params) { + this.params = params.clone(); } /** @@ -53,19 +53,19 @@ public OptionsParser(String[] params) { * {@link #getRequiredIntValue(String)}, and {@link #getOptionalBooleanValue(boolean)}. */ public String nextOption() { - if (mIndex >= mParams.length) { + if (index >= params.length) { // No more parameters left return null; } - String param = mParams[mIndex]; + String param = params[index]; if (!param.startsWith("-")) { // Not an option return null; } - mIndex++; - mLastOptionOriginalForm = param; - mLastOptionValue = null; + index++; + lastOptionOriginalForm = param; + lastOptionValue = null; if (param.startsWith("--")) { // FORMAT: --name value OR --name=value if ("--".equals(param)) { @@ -74,8 +74,8 @@ public String nextOption() { } int valueDelimiterIndex = param.indexOf('='); if (valueDelimiterIndex != -1) { - mLastOptionValue = param.substring(valueDelimiterIndex + 1); - mLastOptionOriginalForm = param.substring(0, valueDelimiterIndex); + lastOptionValue = param.substring(valueDelimiterIndex + 1); + lastOptionOriginalForm = param.substring(0, valueDelimiterIndex); return param.substring("--".length(), valueDelimiterIndex); } else { return param.substring("--".length()); @@ -91,30 +91,30 @@ public String nextOption() { * or dashes. This is intended to be used for referencing the option in error messages. */ public String getOptionOriginalForm() { - return mLastOptionOriginalForm; + return lastOptionOriginalForm; } /** * Returns the value of the current option, throwing an exception if the value is missing. */ public String getRequiredValue(String valueDescription) throws OptionsException { - if (mLastOptionValue != null) { - String result = mLastOptionValue; - mLastOptionValue = null; + if (lastOptionValue != null) { + String result = lastOptionValue; + lastOptionValue = null; return result; } - if (mIndex >= mParams.length) { + if (index >= params.length) { // No more parameters left throw new OptionsException( - valueDescription + " missing after " + mLastOptionOriginalForm); + valueDescription + " missing after " + lastOptionOriginalForm); } - String param = mParams[mIndex]; + String param = params[index]; if ("--".equals(param)) { // End of options marker throw new OptionsException( - valueDescription + " missing after " + mLastOptionOriginalForm); + valueDescription + " missing after " + lastOptionOriginalForm); } - mIndex++; + index++; return param; } @@ -128,7 +128,7 @@ public int getRequiredIntValue(String valueDescription) throws OptionsException return Integer.parseInt(value); } catch (NumberFormatException e) { throw new OptionsException( - valueDescription + " (" + mLastOptionOriginalForm + valueDescription + " (" + lastOptionOriginalForm + ") must be a decimal number: " + value); } } @@ -138,31 +138,31 @@ public int getRequiredIntValue(String valueDescription) throws OptionsException * explicitly specified values. */ public boolean getOptionalBooleanValue(boolean defaultValue) throws OptionsException { - if (mLastOptionValue != null) { + if (lastOptionValue != null) { // --option=value form - String stringValue = mLastOptionValue; - mLastOptionValue = null; + String stringValue = lastOptionValue; + lastOptionValue = null; if ("true".equals(stringValue)) { return true; } else if ("false".equals(stringValue)) { return false; } throw new OptionsException( - "Unsupported value for " + mLastOptionOriginalForm + ": " + stringValue + "Unsupported value for " + lastOptionOriginalForm + ": " + stringValue + ". Only true or false supported."); } // --option (true|false) form OR just --option - if (mIndex >= mParams.length) { + if (index >= params.length) { return defaultValue; } - String stringValue = mParams[mIndex]; + String stringValue = params[index]; if ("true".equals(stringValue)) { - mIndex++; + index++; return true; } else if ("false".equals(stringValue)) { - mIndex++; + index++; return false; } else { return defaultValue; @@ -174,15 +174,15 @@ public boolean getOptionalBooleanValue(boolean defaultValue) throws OptionsExcep * {@link #nextOption()} returns {@code null}. */ public String[] getRemainingParams() { - if (mIndex >= mParams.length) { + if (index >= params.length) { return new String[0]; } - String param = mParams[mIndex]; + String param = params[index]; if ("--".equals(param)) { // Skip end of options marker - return Arrays.copyOfRange(mParams, mIndex + 1, mParams.length); + return Arrays.copyOfRange(params, index + 1, params.length); } else { - return Arrays.copyOfRange(mParams, mIndex, mParams.length); + return Arrays.copyOfRange(params, index, params.length); } } From cbca1bfd39da7943a7a3e80b563c18b10991e2ff Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Thu, 1 Jun 2017 12:53:28 +0800 Subject: [PATCH 10/67] add packer-ng command line application --- cli/build.gradle | 25 ++- .../java/com/mcxiaoke/packer/cli/Helper.java | 111 ++++++++++++ .../java/com/mcxiaoke/packer/cli/Main.java | 167 ++++++++++++++++++ .../packer/cli/{Operator.java => Packer.java} | 2 +- cli/src/main/resources/META-INF/MANIFEST.MF | 3 - .../com/mcxiaoke/packer/cli/help.txt | 34 ++++ .../packer/ng/ArchiveAllApkTask.groovy | 8 +- sample/build.gradle | 9 - 8 files changed, 336 insertions(+), 23 deletions(-) create mode 100644 cli/src/main/java/com/mcxiaoke/packer/cli/Helper.java rename cli/src/main/java/com/mcxiaoke/packer/cli/{Operator.java => Packer.java} (98%) delete mode 100644 cli/src/main/resources/META-INF/MANIFEST.MF create mode 100644 cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt diff --git a/cli/build.gradle b/cli/build.gradle index d9f38be..48b1b82 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -11,25 +11,38 @@ repositories { } apply plugin: 'java' +apply plugin: 'application' sourceCompatibility = 1.7 targetCompatibility = 1.7 dependencies { compile project(":common") - compile 'com.android.tools.build:apksig:2.3.0' + compile 'com.android.tools.build:apksig:2.3.2' } +mainClassName = 'com.mcxiaoke.packer.cli.Main' + task fatJar(type: Jar) { + with jar + from { + configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + } manifest { - attributes 'Implementation-Title': 'PackerNg 2 Executable Jar', + attributes('Implementation-Title': 'PackerNg 2 Executable Jar', 'Implementation-Version': VERSION_NAME, 'Main-Class': 'com.mcxiaoke.packer.cli.Main', - 'Description': 'This is PackerNg 2 executable Jar.' + 'Description': 'This is PackerNg 2 executable Jar.', + 'Owner': 'packer-ng-plugin@mcxiaoke.com', + 'Project': 'https://github.com/mcxiaoke/packer-ng-plugin') } - baseName = 'PackerNg' - from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } - with jar + baseName = 'packer-ng' + +} + +task distJar(type: Copy, dependsOn: fatJar) { + from fatJar.outputs.files + into project.rootProject.file('dist') } // apply from: '../jar.gradle' diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Helper.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Helper.java new file mode 100644 index 0000000..cf6e330 --- /dev/null +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Helper.java @@ -0,0 +1,111 @@ +package com.mcxiaoke.packer.cli; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * User: mcxiaoke + * Date: 2017/5/31 + * Time: 16:52 + */ + +class Helper { + public static List readChannels(final File file) throws IOException { + final List markets = new ArrayList(); + FileReader fr = new FileReader(file); + BufferedReader br = new BufferedReader(fr); + String line; + while ((line = br.readLine()) != null) { + String parts[] = line.split("#"); + if (parts.length > 0) { + final String market = parts[0].trim(); + if (market.length() > 0) { + markets.add(market); + } + } + } + br.close(); + fr.close(); + return markets; + } + + public static void copyFile(File src, File dest) throws IOException { + if (!dest.exists()) { + dest.createNewFile(); + } + FileChannel source = null; + FileChannel destination = null; + try { + source = new FileInputStream(src).getChannel(); + destination = new FileOutputStream(dest).getChannel(); + destination.transferFrom(source, 0, source.size()); + } finally { + if (source != null) { + source.close(); + } + if (destination != null) { + destination.close(); + } + } + } + + public static void deleteAPKs(File dir) { + FilenameFilter filter = new FilenameFilter() { + @Override + public boolean accept(final File dir, final String name) { + return name.toLowerCase().endsWith(".apk"); + } + }; + File[] files = dir.listFiles(filter); + if (files == null || files.length == 0) { + return; + } + for (File file : files) { + file.delete(); + } + } + + public static String getExtName(final String fileName) { + int dot = fileName.lastIndexOf("."); + if (dot > 0) { + return fileName.substring(dot + 1); + } else { + return null; + } + } + + public static String getBaseName(final String fileName) { + int dot = fileName.lastIndexOf("."); + if (dot > 0) { + return fileName.substring(0, dot); + } else { + return fileName; + } + } + + public static void printUsage() { + try { + BufferedReader in = new BufferedReader(new InputStreamReader( + Main.class.getResourceAsStream("help.txt"), + StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) { + System.out.println(line); + } + } catch (IOException e) { + throw new RuntimeException("Failed to read help resource"); + } + } + + +} diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java index 908f2b1..985d21a 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java @@ -1,9 +1,176 @@ package com.mcxiaoke.packer.cli; +import com.mcxiaoke.packer.cli.Options.OptionsException; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + /** * User: mcxiaoke * Date: 2017/5/26 * Time: 15:56 */ public class Main { + + public static final String OUTPUT = "output"; + + public static void main(String[] args) { + if ((args.length == 0) + || ("--help".equals(args[0])) + || ("-h".equals(args[0])) + || "-v".equals(args[0]) + || "--version".equals(args[0])) { + printUsage(); + return; + } + final String cmd = args[0]; + final String[] params = Arrays.copyOfRange(args, 1, args.length); + try { + if ("pack".equals(cmd)) { + pack(params); + } else if ("verify".equals(cmd)) { + verify(params); + } else if ("help".equals(cmd)) { + printUsage(); + } else if ("version".equals(cmd)) { + printUsage(); + } else { + printUsage(); + System.err.println( + "Unsupported command: " + cmd); + } + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + } + + public static void printUsage() { + Helper.printUsage(); + } + + private static void pack(String[] params) throws Exception { + if (params.length == 0) { + printUsage(); + return; + } + System.out.println("========== APK Packer =========="); + // --channels=a,b,c, -c (list mode) + // --channels=@list.txt -c (file mode) + List channels = null; + // --input, -i (input apk file) + File apkFile = null; + // --output, -o (output directory) + File outputDir = null; + Options optionsParser = new Options(params); + String name; + String form = null; + while ((name = optionsParser.nextOption()) != null) { + form = optionsParser.getOptionOriginalForm(); + if (("help".equals(name)) || ("h".equals(name))) { + printUsage(); + return; + } else if ("channels".equals(name) + || "c".equals(name)) { + String value = optionsParser.getRequiredValue("Channels file(@) or list(,)."); + if (value.startsWith("@")) { + value = value.substring(1); + channels = Helper.readChannels(new File(value)); + } else { + channels = Arrays.asList(value.split(",")); + } + } else if ("input".equals(name) + || "i".equals(name)) { + String value = optionsParser.getRequiredValue("Input APK file"); + apkFile = new File(value); + } else if ("output".equals(name) + || "o".equals(name)) { + String value = optionsParser.getRequiredValue("Output Directory"); + outputDir = new File(value); + } else { + printUsage(); + System.err.println( + "Unsupported option: " + form); + } + } + params = optionsParser.getRemainingParams(); + if (apkFile == null) { + if (params.length < 1) { + throw new OptionsException("Missing Input APK"); + } + apkFile = new File(params[0]); + } + if (outputDir == null) { + outputDir = new File(OUTPUT); + } + doPack(apkFile, channels, outputDir); + } + + private static void doPack(File apkFile, List channels, File outputDir) + throws IOException { + if (apkFile == null + || !apkFile.exists() + || !apkFile.isFile()) { + throw new IOException("Invalid Input APK: " + apkFile); + } + if (!Packer.verifyApk(apkFile)) { + throw new IOException("Invalid Signature: " + apkFile); + } + if (outputDir.exists()) { + Helper.deleteAPKs(outputDir); + } else { + outputDir.mkdirs(); + } + System.out.println("File: " + apkFile.getAbsolutePath()); + System.out.println("Channels:" + Arrays.toString(channels.toArray())); + System.out.println("OutputDir:" + outputDir.getAbsolutePath()); + final String fileName = apkFile.getName(); + final String baseName = Helper.getBaseName(fileName); + final String extName = Helper.getExtName(fileName); + for (final String channel : channels) { + final String apkName = String.format(Locale.US, + "%s-%s.%s", baseName, channel, extName); + File destFile = new File(outputDir, apkName); + Helper.copyFile(apkFile, destFile); + Packer.writeChannel(destFile, channel); + if (Packer.verifyChannel(destFile, channel)) { + System.out.println("Generating APK: " + apkName); + } else { + destFile.delete(); + throw new IOException("Failed to verify APK: " + apkName); + } + } + System.out.println("All APK files saved to " + outputDir.getAbsolutePath()); + } + + private static void verify(String[] params) throws Exception { + if (params.length == 0) { + printUsage(); + return; + } + System.out.println("========== APK Verify =========="); + if (params.length < 1) { + throw new IllegalArgumentException("Missing Input APK"); + } + File apkFile = new File(params[0]); + doVerify(apkFile); + } + + private static void doVerify(File apkFile) throws IOException { + if (apkFile == null + || !apkFile.exists() + || !apkFile.isFile()) { + throw new IOException("Invalid Input APK: " + apkFile); + } + final boolean verified = Packer.verifyApk(apkFile); + final String channel = Packer.readChannel(apkFile); + System.out.println("File: " + apkFile); + System.out.println("Signed:" + verified); + System.out.println("Channel:" + channel); + } + + } diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Operator.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Packer.java similarity index 98% rename from cli/src/main/java/com/mcxiaoke/packer/cli/Operator.java rename to cli/src/main/java/com/mcxiaoke/packer/cli/Packer.java index f5e2f67..8a3ac7d 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/Operator.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Packer.java @@ -15,7 +15,7 @@ * Date: 2017/5/26 * Time: 16:21 */ -public class Operator { +public class Packer { public static void writeChannel(File apkFile, String channel) throws IOException { PackerParser.create(apkFile).writeChannel(channel); diff --git a/cli/src/main/resources/META-INF/MANIFEST.MF b/cli/src/main/resources/META-INF/MANIFEST.MF deleted file mode 100644 index 077b411..0000000 --- a/cli/src/main/resources/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Main-Class: com.mcxiaoke.packer.helper.PackerNg2 - diff --git a/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt b/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt new file mode 100644 index 0000000..5cf110e --- /dev/null +++ b/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt @@ -0,0 +1,34 @@ + +INTRODUCTION + + PackerNg is a tool for add channel information to Android APK files. + +PROJECT + + URL: https://github.com/mcxiaoke/packer-ng-plugin + Email: packer-ng-plugin@mcxiaoke.com + +USAGE + + packer-ng [options] + packer-ng --help [-h] + packer-ng pack --channels --output apk + packer-ng verify apk + +EXAMPLE + +pack Add channel info to the provided APK + + packer-ng pack --channels=ch1,ch2,ch3 --output=archives app.apk + packer-ng pack --channels=@file.txt --output=archives app.apk + + --channels=@file.txt - using channels from the provided file. + --channels=ch1,ch2,ch3 - using channels from the provided list. + --output=archives - output directory for save final APK files. + --input=file - base APK file for add channel information. + +verify Check whether signatures and channel of the provided APK is valid. + + packer-ng verify app.apk + + diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy index 7238722..323426e 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy @@ -2,7 +2,7 @@ package com.mcxiaoke.packer.ng import com.android.build.gradle.api.BaseVariant import com.android.builder.model.SigningConfig -import com.mcxiaoke.packer.cli.Operator +import com.mcxiaoke.packer.cli.Packer import groovy.io.FileType import groovy.text.SimpleTemplateEngine import org.gradle.api.DefaultTask @@ -68,7 +68,7 @@ class ArchiveAllApkTask extends DefaultTask { void checkApkSignature(File file) throws GradleException { File apkPath = project.rootDir.toPath().relativize(file.toPath()).toFile() - boolean apkVerified = Operator.verifyApk(apkPath) + boolean apkVerified = Packer.verifyApk(apkPath) if (!apkVerified) { throw new GradleException(":${name} " + "apk ${apkPath} not v2 signed, please check your signingConfig!") @@ -102,10 +102,10 @@ class ArchiveAllApkTask extends DefaultTask { File tempFile = new File(outputDir, market + ".tmp") copyTo(originalFile, tempFile) try { - Operator.writeChannel(tempFile, market) + Packer.writeChannel(tempFile, market) String apkName = buildApkName(theVariant, market, tempFile) File finalFile = new File(outputDir, apkName) - if (Operator.verifyChannel(tempFile, market)) { + if (Packer.verifyChannel(tempFile, market)) { println(":${project.name}:${name} Generating apk for ${market}") tempFile.renameTo(finalFile) } else { diff --git a/sample/build.gradle b/sample/build.gradle index c25c287..424e836 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -102,15 +102,6 @@ android { } - productFlavors { - cat { - } - - dog { - signingConfig signingConfigs.release - } - } - lintOptions { abortOnError false htmlReport true From ec4fed3ea0db318e6d8dca13e2466ecf5749f55b Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 2 Jun 2017 11:37:23 +0800 Subject: [PATCH 11/67] plugin: refactory and packaging support v2 signature scheme --- plugin/build.gradle | 8 + .../packer/ng/ArchiveAllApkTask.groovy | 162 ---------------- ...anArchivesTask.groovy => CleanTask.groovy} | 6 +- .../com/mcxiaoke/packer/ng/Extension.groovy | 42 ++++ .../com/mcxiaoke/packer/ng/MainPlugin.groovy | 70 +++++++ .../com/mcxiaoke/packer/ng/PackTask.groovy | 182 ++++++++++++++++++ .../packer/ng/PackerNgExtension.groovy | 48 ----- .../mcxiaoke/packer/ng/PackerNgPlugin.groovy | 153 --------------- .../META-INF/gradle-plugins/packer.properties | 1 + .../packer/ng/AndroidPackerPluginTest.groovy | 5 - 10 files changed, 306 insertions(+), 371 deletions(-) delete mode 100644 plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy rename plugin/src/main/groovy/com/mcxiaoke/packer/ng/{CleanArchivesTask.groovy => CleanTask.groovy} (78%) create mode 100644 plugin/src/main/groovy/com/mcxiaoke/packer/ng/Extension.groovy create mode 100644 plugin/src/main/groovy/com/mcxiaoke/packer/ng/MainPlugin.groovy create mode 100644 plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackTask.groovy delete mode 100644 plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackerNgExtension.groovy delete mode 100644 plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackerNgPlugin.groovy create mode 100644 plugin/src/main/resources/META-INF/gradle-plugins/packer.properties delete mode 100644 plugin/src/test/groovy/com/mcxiaoke/packer/ng/AndroidPackerPluginTest.groovy diff --git a/plugin/build.gradle b/plugin/build.gradle index f4645f4..b072320 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -1,3 +1,11 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + } +} + apply plugin: 'groovy' sourceCompatibility = 1.7 diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy deleted file mode 100644 index 323426e..0000000 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy +++ /dev/null @@ -1,162 +0,0 @@ -package com.mcxiaoke.packer.ng - -import com.android.build.gradle.api.BaseVariant -import com.android.builder.model.SigningConfig -import com.mcxiaoke.packer.cli.Packer -import groovy.io.FileType -import groovy.text.SimpleTemplateEngine -import org.gradle.api.DefaultTask -import org.gradle.api.GradleException -import org.gradle.api.InvalidUserDataException -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.TaskAction - -import java.text.SimpleDateFormat - -/** - * User: mcxiaoke - * Date: 15/11/23 - * Time: 14:40 - */ -class ArchiveAllApkTask extends DefaultTask { - static final TAG = PackerNgPlugin.TAG - - @Input - BaseVariant theVariant - - @Input - PackerNgExtension theExtension - - @Input - List theMarkets - - ArchiveAllApkTask() { - setDescription('modify original apk file and move to release dir') - } - - @TaskAction - void showMessage() { - project.logger.info("${name}: ${description}") - } - - void checkMarkets() throws GradleException { - if (theMarkets == null || theMarkets.isEmpty()) { - throw new InvalidUserDataException(":${name} " + - "no markets found, please check your market file!") - } - } - - SigningConfig getSigningConfig() { - def config1 = theVariant.buildType.signingConfig - def config2 = theVariant.mergedFlavor.signingConfig - return config1 == null ? config2 : config1 - } - - void checkSigningConfig() throws GradleException { - logger.info(":${name} buildType.signingConfig = " + - "${theVariant.buildType.signingConfig}") - logger.info(":${name} mergedFlavor.signingConfig = " + - "${theVariant.mergedFlavor.signingConfig}") - def signingConfig = getSigningConfig() - if (theExtension.checkSigningConfig && signingConfig == null) { - throw new GradleException(":${project.name}:${name} " + - "signingConfig not found, task aborted, " + - "please check your signingConfig!") - } - - } - - void checkApkSignature(File file) throws GradleException { - File apkPath = project.rootDir.toPath().relativize(file.toPath()).toFile() - boolean apkVerified = Packer.verifyApk(apkPath) - if (!apkVerified) { - throw new GradleException(":${name} " + - "apk ${apkPath} not v2 signed, please check your signingConfig!") - } - } - - @TaskAction - void modify() { - logger.info("====================PACKER APK TASK BEGIN====================") - checkMarkets() - checkSigningConfig() - File originalFile = theVariant.outputs[0].outputFile - checkApkSignature(originalFile) - File outputDir = theExtension.archiveOutput - File apkPath = project.rootDir.toPath().relativize(originalFile.toPath()).toFile() - println(":${project.name}:${name} apk: ${apkPath}") - logger.info(":${name} output dir:${outputDir.absolutePath}") - if (!outputDir.exists()) { - outputDir.mkdirs() - } else { - logger.info(":${name} delete old apks in ${outputDir.absolutePath}") - // delete old apks - outputDir.eachFile(FileType.FILES) { file -> - if (file.getName().endsWith(".apk")) { - file.delete() - } - } - } - logger.info(":${project.name}:${name} markets:[${theMarkets.join(', ')}]") - for (String market : theMarkets) { - File tempFile = new File(outputDir, market + ".tmp") - copyTo(originalFile, tempFile) - try { - Packer.writeChannel(tempFile, market) - String apkName = buildApkName(theVariant, market, tempFile) - File finalFile = new File(outputDir, apkName) - if (Packer.verifyChannel(tempFile, market)) { - println(":${project.name}:${name} Generating apk for ${market}") - tempFile.renameTo(finalFile) - } else { - throw new GradleException(":${name} ${market} apk verify failed.") - } - } catch (IOException ex) { - throw new GradleException(":${name} ${market} apk generate failed.", ex) - } finally { - tempFile.delete() - } - } - println(":${project.name}:${name} all ${theMarkets.size()} apks saved to ${outputDir.path}") - println("\nPackerNg Build Successful!") - logger.info("====================PACKER APK TASK END====================") - } - - /** - * build human readable apk name - * @param variant Variant - * @return final apk name - */ - String buildApkName(variant, market, apkFile) { - def buildTime = new SimpleDateFormat('yyyyMMdd-HHmmss').format(new Date()) - File file = apkFile - def fileMD5 = HASH.md5(file) - def fileSHA1 = HASH.sha1(file) - def nameMap = [ - 'appName' : project.name, - 'projectName': project.rootProject.name, - 'fileMD5' : fileMD5, - 'fileSHA1' : fileSHA1, - 'flavorName' : market, - 'buildType' : variant.buildType.name, - 'versionName': variant.versionName, - 'versionCode': variant.versionCode, - 'appPkg' : variant.applicationId, - 'buildTime' : buildTime - ] - - def defaultTemplate = PackerNgExtension.DEFAULT_NAME_TEMPLATE - def engine = new SimpleTemplateEngine() - def template = theExtension.archiveNameFormat == null ? defaultTemplate : theExtension.archiveNameFormat - def fileName = engine.createTemplate(template).make(nameMap).toString() - return fileName + '.apk' - } - - static void copyTo(File src, File dest) { - def input = src.newInputStream() - def output = dest.newOutputStream() - output << input - input.close() - output.close() - } -} diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanArchivesTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanTask.groovy similarity index 78% rename from plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanArchivesTask.groovy rename to plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanTask.groovy index 747bc58..ad8469e 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanArchivesTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanTask.groovy @@ -9,13 +9,13 @@ import org.gradle.api.tasks.TaskAction * Date: 14/12/19 * Time: 11:29 */ -class CleanArchivesTask extends DefaultTask { +class CleanTask extends DefaultTask { @Input File target - CleanArchivesTask() { - setDescription('clean all apk archives in output dir') + CleanTask() { + setDescription('clean all files in output dir') } @TaskAction diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Extension.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Extension.groovy new file mode 100644 index 0000000..1efdfa8 --- /dev/null +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Extension.groovy @@ -0,0 +1,42 @@ +package com.mcxiaoke.packer.ng + +import org.gradle.api.Project + +// Android Packer Plugin Extension +class Extension { + static final String DEFAULT_NAME_TEMPLATE = '${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}' + + File archiveOutput + + /** + * file name template string + * + * Available vars: + * 1. projectName + * 2. appName + * 3. appPkg + * 4. buildType + * 5. channel + * 6. versionName + * 7. versionCode + * 8. buildTime + * 9. fileMD5 + * 10. fileSHA1 + * + * default value: '${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}' + */ + String archiveNameFormat + + List channelList; + + File channelFile; + + Extension(Project project) { + archiveOutput = new File(project.rootProject.buildDir, "archives") + archiveNameFormat = DEFAULT_NAME_TEMPLATE + channelList = null + channelFile = null + } + + +} diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/MainPlugin.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/MainPlugin.groovy new file mode 100644 index 0000000..5aeee84 --- /dev/null +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/MainPlugin.groovy @@ -0,0 +1,70 @@ +package com.mcxiaoke.packer.ng + +import com.android.build.gradle.api.BaseVariant +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.ProjectConfigurationException + +// Android PackerNg Plugin Source +class MainPlugin implements Plugin { + static final String TAG = "PackerNg" + static final String PLUGIN_NAME = "packer" + + Project project + + @Override + void apply(Project project) { + this.project = project + if (!project.plugins.hasPlugin("com.android.application")) { + throw new ProjectConfigurationException( + "the android plugin must be applied", null) + } + project.configurations.create(PLUGIN_NAME).extendsFrom(project.configurations.compile) + project.extensions.create(PLUGIN_NAME, Extension, project) + project.afterEvaluate { + addCleanTask() + project.android.applicationVariants.all { BaseVariant variant -> + addPackTask(variant) + } + } + } + + void addPackTask(BaseVariant v) { + debug("addPackTask() for ${v.name}") + def packTask = project.task("apk${v.name.capitalize()}", + type: PackTask) { + variant = v + extension = project.packer + dependsOn v.assemble + } + + debug("addPackTask() new variant task:${packTask.name}") + + def buildTypeName = v.buildType.name + if (v.name != buildTypeName) { + def taskName = "apk${buildTypeName.capitalize()}" + def task = project.tasks.findByName(taskName) + if (task == null) { + debug("addPackTask() new build type task:${taskName}") + project.task(taskName, dependsOn: packTask) + } + } + } + + void addCleanTask() { + def output = project.packer.archiveOutput + debug("addCleanTask() create clean archived apks task, path:${output}") + def task = project.task("cleanPackOutputs", + type: CleanTask) { + target = output + } + project.getTasksByName("clean", true)?.each { + it.dependsOn task + } + } + + void debug(String msg) { + project.logger.info(msg) + } + +} diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackTask.groovy new file mode 100644 index 0000000..b3f72d2 --- /dev/null +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackTask.groovy @@ -0,0 +1,182 @@ +package com.mcxiaoke.packer.ng + +import com.android.build.gradle.api.BaseVariant +import com.mcxiaoke.packer.cli.Packer +import groovy.io.FileType +import groovy.text.SimpleTemplateEngine +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.InvalidUserDataException +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +import java.text.SimpleDateFormat + +/** + * User: mcxiaoke + * Date: 15/11/23 + * Time: 14:40 + */ +class PackTask extends DefaultTask { + + @Input + BaseVariant variant + + @Input + Extension extension + + PackTask() { + setDescription('pack original apk file and move to output dir') + } + + @TaskAction + void showMessage() { + project.logger.info("${name}: ${description}") + } + + void checkChannels(List channels) throws GradleException { + if (channels == null || channels.isEmpty()) { + throw new InvalidUserDataException(":${name} " + + "no channels found, please check your market file!") + } + } + + void checkSignature(File file) throws GradleException { + boolean apkVerified = Packer.verifyApk(file) + if (!apkVerified) { + throw new GradleException(":${name} " + + "apk ${file} not v2 signed, please check your signingConfig!") + } + } + + List getChannels() { + // -P channels=ch1,ch2,ch3 + // -P channels=@channels.txt + // channelList = [ch1,ch2,ch3] + // channelFile = project.file("channels.txt") + List channels = [] + if (project.hasProperty("channels")) { + def pv = project.property("channels").toString(); + logger.info(":${project.name} channels property: ${pv}") + if (pv.startsWith("@")) { + def fp = pv.substring(1) + if (fp != null) { + File f = new File(project.rootDir, fp) + channels = readChannels(f) + } + } else { + channels = pv.split(",") + } + } else if (extension.channelList != null) { + channels = extension.channelList + logger.info(":${project.name} ext.channelList: ${extension.channelList}") + } else { + File f; + if (extension.channelFile != null) { + f = extension.channelFile + } else { + f = new File(project.rootDir, "channels.txt") + } + logger.info(":${project.name} extension.channelFile: ${f}") + channels = readChannels(f) + } + if (channels == null) { + channels = [] + } + return channels + } + + List readChannels(File file) { + List channels = [] + file.eachLine { line, number -> + String[] parts = line.split('#') + if (parts && parts[0]) { + def c = parts[0].trim() + if (c) { + channels.add(c) + } + } else { + logger.info(":${project.name} skip invalid #${number}:'${line}'") + } + } + return channels + } + + @TaskAction + void pack() { + logger.info("====================PACKER NG TASK BEGIN====================") + File originalFile = variant.outputs[0].outputFile + checkSignature(originalFile) + List channels = getChannels(); + checkChannels(channels) + File outputDir = extension.archiveOutput + File apkPath = project.rootDir.toPath().relativize(originalFile.toPath()).toFile() + println(":${project.name}:${name} apk: ${apkPath}") + logger.info(":${name} output dir:${outputDir.absolutePath}") + if (!outputDir.exists()) { + outputDir.mkdirs() + } else { + logger.info(":${name} delete old apks in ${outputDir.absolutePath}") + // delete old APKs + outputDir.eachFile(FileType.FILES) { file -> + if (file.getName().endsWith(".apk")) { + file.delete() + } + } + } + println(":${project.name}:${name} channels:[${channels.join(', ')}]") + for (String channel : channels) { + File tempFile = new File(outputDir, channel + ".tmp") + copyTo(originalFile, tempFile) + try { + Packer.writeChannel(tempFile, channel) + String apkName = buildApkName(variant, channel, tempFile) + File finalFile = new File(outputDir, apkName) + if (Packer.verifyChannel(tempFile, channel)) { + println(":${project.name}:${name} Generating apk for ${channel}") + tempFile.renameTo(finalFile) + } else { + throw new GradleException(":${name} ${channel} apk verify failed.") + } + } catch (IOException ex) { + throw new GradleException(":${name} ${channel} apk generate failed.", ex) + } finally { + tempFile.delete() + } + } + println(":${project.name}:${name} all ${channels.size()} apks saved to ${outputDir.path}") + println("\nPackerNg Build Successful!") + logger.info("====================PACKER NG TASK END====================") + } + + String buildApkName(variant, channel, apkFile) { + def buildTime = new SimpleDateFormat('yyyyMMdd-HHmmss').format(new Date()) + File file = apkFile + def fileSHA1 = HASH.sha1(file) + def nameMap = [ + 'appName' : project.name, + 'projectName': project.rootProject.name, + 'fileSHA1' : fileSHA1, + 'channel' : channel, + 'buildType' : variant.buildType.name, + 'versionName': variant.versionName, + 'versionCode': variant.versionCode, + 'appPkg' : variant.applicationId, + 'buildTime' : buildTime + ] + + def dt = Extension.DEFAULT_NAME_TEMPLATE + def engine = new SimpleTemplateEngine() + def template = extension.archiveNameFormat == null ? dt : extension.archiveNameFormat + def fileName = engine.createTemplate(template).make(nameMap).toString() + return fileName + '.apk' + } + + static void copyTo(File src, File dest) { + def input = src.newInputStream() + def output = dest.newOutputStream() + output << input + input.close() + output.close() + } +} diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackerNgExtension.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackerNgExtension.groovy deleted file mode 100644 index cab7a7c..0000000 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackerNgExtension.groovy +++ /dev/null @@ -1,48 +0,0 @@ -package com.mcxiaoke.packer.ng - -import org.gradle.api.Project - -// Android Packer Plugin Extension -class PackerNgExtension { - static final String DEFAULT_NAME_TEMPLATE = '${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}' - - /** - * archive task output dir - */ - File archiveOutput - - File tempOutput - - boolean checkSigningConfig - - boolean checkZipAlign - - /** - * file name template string - * - * Available vars: - * 1. projectName - * 2. appName - * 3. appPkg - * 4. buildType - * 5. flavorName - * 6. versionName - * 7. versionCode - * 8. buildTime - * 9. fileMD5 - * 10. fileSHA1 - * - * default value: '${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}' - */ - String archiveNameFormat - - PackerNgExtension(Project project) { - archiveOutput = new File(project.rootProject.buildDir, "archives") - tempOutput = new File(project.rootProject.buildDir, "temp") - archiveNameFormat = DEFAULT_NAME_TEMPLATE - checkSigningConfig = false - checkZipAlign = false - } - - -} diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackerNgPlugin.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackerNgPlugin.groovy deleted file mode 100644 index 370361d..0000000 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackerNgPlugin.groovy +++ /dev/null @@ -1,153 +0,0 @@ -package com.mcxiaoke.packer.ng - -import com.android.build.gradle.api.BaseVariant -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.ProjectConfigurationException - -// Android Multi Packer Plugin Source -class PackerNgPlugin implements Plugin { - static final String TAG = "PackerNg" - static final String PLUGIN_NAME = "packer" - static final String P_MARKET = "market" - - Project project - PackerNgExtension modifierExtension - List markets; - - @Override - void apply(Project project) { - this.project = project - if (!project.plugins.hasPlugin("com.android.application")) { - throw new ProjectConfigurationException("the android plugin must be applied", null) - } - applyExtension() - parseMarkets() - applyPluginTasks() - } - - void applyExtension() { - // setup plugin and extension - project.configurations.create(PLUGIN_NAME).extendsFrom(project.configurations.compile) - this.modifierExtension = project.extensions.create(PLUGIN_NAME, PackerNgExtension, project) - } - - void applyPluginTasks() { - project.afterEvaluate { - checkCleanTask() - debug(":${project.name} flavors: ${project.android.productFlavors.collect { it.name }}") - //applySigningConfigs() - project.android.applicationVariants.all { BaseVariant variant -> - checkPackerNgTask(variant) - } - } - } - -/** - * parse markets file - * @param project Project - * @return found markets file - */ - boolean parseMarkets() { - markets = new ArrayList(); - - if (!project.hasProperty(P_MARKET)) { - debug("parseMarkets() market property not found, ignore") - return false - } - - // check markets file exists - def marketsFilePath = project.property(P_MARKET).toString() - if (!marketsFilePath) { - println(":${project.name} markets property not found, using default") - // if not set, use default ./markets.txt - marketsFilePath = "markets.txt" - } - - File file = project.rootProject.file(marketsFilePath) - if (!file.exists() || !file.isFile() || !file.canRead()) { - throw new IllegalArgumentException("Invalid market file: ${file.absolutePath}") - } - println(":${project.name} market: ${file.absolutePath}") - markets = readMarkets(file) - debug(":${project.name} found markets:$markets") - return true - } - - List readMarkets(File file) { - // add all markets - List allMarkets = [] - file.eachLine { line, number -> - String[] parts = line.split('#') - if (parts && parts[0]) { - def market = parts[0].trim() - if (market) { - allMarkets.add(market) - } - } else { - debug(":${project.name} skip invalid market line ${number}:'${line}'") - } - } - return allMarkets - } - -/** - * add archiveApk tasks - * @param variant current Variant - */ - void checkPackerNgTask(BaseVariant variant) { - debug("checkPackerNgTask() for ${variant.name}") - def File inputFile = variant.outputs[0].outputFile - def File tempDir = modifierExtension.tempOutput - def File outputDir = modifierExtension.archiveOutput - debug("checkPackerNgTask() input: ${inputFile}") - debug("checkPackerNgTask() temp: ${tempDir}") - debug("checkPackerNgTask() output: ${outputDir}") - def archiveTask = project.task("apk${variant.name.capitalize()}", - type: ArchiveAllApkTask) { - theVariant = variant - theExtension = modifierExtension - theMarkets = markets - dependsOn variant.assemble - } - - debug("checkPackerNgTask() new variant task:${archiveTask.name}") - - def buildTypeName = variant.buildType.name - if (variant.name != buildTypeName) { - def taskName = "apk${buildTypeName.capitalize()}" - def task = project.tasks.findByName(taskName) - if (task == null) { - debug("checkPackerNgTask() new build type task:${taskName}") - task = project.task(taskName, dependsOn: archiveTask) - } - } - } - - /** - * add cleanArchives task if not added - * @return task - */ - void checkCleanTask() { - def output = modifierExtension.archiveOutput - debug("checkCleanTask() create clean archived apks task, path:${output}") - def task = project.task("cleanApks", - type: CleanArchivesTask) { - target = output - } - - project.getTasksByName("clean", true)?.each { - it.dependsOn task - } - } - -/** - * print debug messages - * @param msg msg - * @param vars vars - */ - void debug(String msg) { - project.logger.info(msg) - } - -} diff --git a/plugin/src/main/resources/META-INF/gradle-plugins/packer.properties b/plugin/src/main/resources/META-INF/gradle-plugins/packer.properties new file mode 100644 index 0000000..493b796 --- /dev/null +++ b/plugin/src/main/resources/META-INF/gradle-plugins/packer.properties @@ -0,0 +1 @@ +implementation-class=com.mcxiaoke.packer.ng.MainPlugin \ No newline at end of file diff --git a/plugin/src/test/groovy/com/mcxiaoke/packer/ng/AndroidPackerPluginTest.groovy b/plugin/src/test/groovy/com/mcxiaoke/packer/ng/AndroidPackerPluginTest.groovy deleted file mode 100644 index f9a8cae..0000000 --- a/plugin/src/test/groovy/com/mcxiaoke/packer/ng/AndroidPackerPluginTest.groovy +++ /dev/null @@ -1,5 +0,0 @@ -package com.mcxiaoke.packer.ng - -class AndroidPackerPluginTest { - -} From 7cecc34f9c1378a9e3def2286fc688943ff5811a Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 2 Jun 2017 11:37:56 +0800 Subject: [PATCH 12/67] fix sample --- .gitignore | 2 +- build.gradle | 8 +++++--- gradle.properties | 2 +- sample/build.gradle | 14 +++++++------- .../com/mcxiaoke/packer/samples/MainActivity.java | 5 ++--- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 052524a..cc165d3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ build/ apks/ repo/ +dist/ *.iml *.apk *.pyc .DS_Store -packer.properties diff --git a/build.gradle b/build.gradle index bf928f0..96e57f6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,11 @@ - ext { compileSdkVersion = 25 buildToolsVersion = "25.0.3" minSdkVersion = 14 targetSdkVersion = 22 - versionName = "1.1.0-SNAPSHIT" + versionName = "1.1.0-SNAPSHOT" versionCode = 110 -} \ No newline at end of file +} + +group = GROUP +version = VERSION_NAME diff --git a/gradle.properties b/gradle.properties index 1230446..f97e734 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.0.1-SNAPSHOT +VERSION_NAME=1.0.13-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/sample/build.gradle b/sample/build.gradle index 424e836..8b0e9ed 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,10 +1,10 @@ buildscript { - ext.packer_version = '1.0.1-SNAPSHOT' + ext.packer_version = '1.0.13-SNAPSHOT' repositories { maven { url '/tmp/repo/' } jcenter() - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } dependencies { @@ -24,7 +24,7 @@ apply plugin: 'packer' // https://code.google.com/p/android/issues/detail?id=171089 dependencies { - // compile project(':helper') + // compile project(':helper') compile "com.mcxiaoke.packer-ng:helper:$packer_version" compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:support-v4:25.3.1' @@ -36,10 +36,10 @@ dependencies { } packer { - checkSigningConfig = false - checkZipAlign = false - archiveNameFormat = '${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}-${fileMD5}' - archiveOutput = file(new File(project.rootProject.buildDir.path, "myapks")) + archiveNameFormat = '${channel}-${buildType}-v${versionName}-${versionCode}' + archiveOutput = new File(project.rootProject.buildDir, "myapks") + channelFile = new File(project.rootDir, "markets.txt") + channelList = ['Douban', 'Google', '中文市场', 'Some Market', '!@#$%^', '20070601'] } android { diff --git a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java index 2acefe2..90ea60b 100644 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java +++ b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java @@ -24,7 +24,6 @@ import com.mcxiaoke.next.utils.AndroidUtils; import com.mcxiaoke.next.utils.LogUtils; import com.mcxiaoke.next.utils.StringUtils; -import com.mcxiaoke.packer.helper.PackerNg; import com.mcxiaoke.packer.samples.BuildConfig; import com.mcxiaoke.packer.samples.R; @@ -64,8 +63,8 @@ private void addAppInfoSection() { StringBuilder builder = new StringBuilder(); builder.append("[AppInfo]\n"); builder.append("SourceDir: ").append(getSourceDir(this)).append("\n"); - builder.append("Market: ").append(PackerNg.getChannel(this)).append("\n"); - builder.append("MarketInfo: ").append(PackerNg.getChannel(this)).append("\n"); +// builder.append("Market: ").append(PackerNg.getChannel(this)).append("\n"); +// builder.append("MarketInfo: ").append(PackerNg.getChannel(this)).append("\n"); builder.append("Name: ").append(getString(info.labelRes)).append("\n"); builder.append("Package: ").append(BuildConfig.APPLICATION_ID).append("\n"); builder.append("VersionCode: ").append(BuildConfig.VERSION_CODE).append("\n"); From f9716f81576bbf8f2ec77167b62e0c1538d40dde Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 2 Jun 2017 11:53:07 +0800 Subject: [PATCH 13/67] plugin: rename classes --- .../main/groovy/com/mcxiaoke/packer/ng/CleanTask.groovy | 7 +------ .../ng/{Extension.groovy => GradleExtension.groovy} | 4 ++-- .../packer/ng/{MainPlugin.groovy => GradlePlugin.groovy} | 6 +++--- .../packer/ng/{PackTask.groovy => GradleTask.groovy} | 8 ++++---- .../resources/META-INF/gradle-plugins/packer.properties | 2 +- 5 files changed, 11 insertions(+), 16 deletions(-) rename plugin/src/main/groovy/com/mcxiaoke/packer/ng/{Extension.groovy => GradleExtension.groovy} (93%) rename plugin/src/main/groovy/com/mcxiaoke/packer/ng/{MainPlugin.groovy => GradlePlugin.groovy} (92%) rename plugin/src/main/groovy/com/mcxiaoke/packer/ng/{PackTask.groovy => GradleTask.groovy} (97%) diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanTask.groovy index ad8469e..f0cab9a 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanTask.groovy @@ -15,12 +15,7 @@ class CleanTask extends DefaultTask { File target CleanTask() { - setDescription('clean all files in output dir') - } - - @TaskAction - void showMessage() { - logger.info("${name}: ${description}") + description = 'clean all files in output dir' } @TaskAction diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Extension.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy similarity index 93% rename from plugin/src/main/groovy/com/mcxiaoke/packer/ng/Extension.groovy rename to plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy index 1efdfa8..2eba3af 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Extension.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy @@ -3,7 +3,7 @@ package com.mcxiaoke.packer.ng import org.gradle.api.Project // Android Packer Plugin Extension -class Extension { +class GradleExtension { static final String DEFAULT_NAME_TEMPLATE = '${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}' File archiveOutput @@ -31,7 +31,7 @@ class Extension { File channelFile; - Extension(Project project) { + GradleExtension(Project project) { archiveOutput = new File(project.rootProject.buildDir, "archives") archiveNameFormat = DEFAULT_NAME_TEMPLATE channelList = null diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/MainPlugin.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy similarity index 92% rename from plugin/src/main/groovy/com/mcxiaoke/packer/ng/MainPlugin.groovy rename to plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy index 5aeee84..c3cdc4b 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/MainPlugin.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy @@ -6,7 +6,7 @@ import org.gradle.api.Project import org.gradle.api.ProjectConfigurationException // Android PackerNg Plugin Source -class MainPlugin implements Plugin { +class GradlePlugin implements Plugin { static final String TAG = "PackerNg" static final String PLUGIN_NAME = "packer" @@ -20,7 +20,7 @@ class MainPlugin implements Plugin { "the android plugin must be applied", null) } project.configurations.create(PLUGIN_NAME).extendsFrom(project.configurations.compile) - project.extensions.create(PLUGIN_NAME, Extension, project) + project.extensions.create(PLUGIN_NAME, GradleExtension, project) project.afterEvaluate { addCleanTask() project.android.applicationVariants.all { BaseVariant variant -> @@ -32,7 +32,7 @@ class MainPlugin implements Plugin { void addPackTask(BaseVariant v) { debug("addPackTask() for ${v.name}") def packTask = project.task("apk${v.name.capitalize()}", - type: PackTask) { + type: GradleTask) { variant = v extension = project.packer dependsOn v.assemble diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy similarity index 97% rename from plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackTask.groovy rename to plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy index b3f72d2..e89f5e3 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy @@ -17,15 +17,15 @@ import java.text.SimpleDateFormat * Date: 15/11/23 * Time: 14:40 */ -class PackTask extends DefaultTask { +class GradleTask extends DefaultTask { @Input BaseVariant variant @Input - Extension extension + GradleExtension extension - PackTask() { + GradleTask() { setDescription('pack original apk file and move to output dir') } @@ -165,7 +165,7 @@ class PackTask extends DefaultTask { 'buildTime' : buildTime ] - def dt = Extension.DEFAULT_NAME_TEMPLATE + def dt = GradleExtension.DEFAULT_NAME_TEMPLATE def engine = new SimpleTemplateEngine() def template = extension.archiveNameFormat == null ? dt : extension.archiveNameFormat def fileName = engine.createTemplate(template).make(nameMap).toString() diff --git a/plugin/src/main/resources/META-INF/gradle-plugins/packer.properties b/plugin/src/main/resources/META-INF/gradle-plugins/packer.properties index 493b796..3521d98 100644 --- a/plugin/src/main/resources/META-INF/gradle-plugins/packer.properties +++ b/plugin/src/main/resources/META-INF/gradle-plugins/packer.properties @@ -1 +1 @@ -implementation-class=com.mcxiaoke.packer.ng.MainPlugin \ No newline at end of file +implementation-class=com.mcxiaoke.packer.ng.GradlePlugin \ No newline at end of file From 4b8ff9b15b145eb03a76a53920caa44f24d31f5f Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 2 Jun 2017 11:54:14 +0800 Subject: [PATCH 14/67] remove clean task --- .../com/mcxiaoke/packer/ng/GradlePlugin.groovy | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy index c3cdc4b..320adb2 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy @@ -22,7 +22,6 @@ class GradlePlugin implements Plugin { project.configurations.create(PLUGIN_NAME).extendsFrom(project.configurations.compile) project.extensions.create(PLUGIN_NAME, GradleExtension, project) project.afterEvaluate { - addCleanTask() project.android.applicationVariants.all { BaseVariant variant -> addPackTask(variant) } @@ -51,18 +50,6 @@ class GradlePlugin implements Plugin { } } - void addCleanTask() { - def output = project.packer.archiveOutput - debug("addCleanTask() create clean archived apks task, path:${output}") - def task = project.task("cleanPackOutputs", - type: CleanTask) { - target = output - } - project.getTasksByName("clean", true)?.each { - it.dependsOn task - } - } - void debug(String msg) { project.logger.info(msg) } From 0248133177e6cabb05189e1e295e9530b4f7bc00 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 2 Jun 2017 17:06:34 +0800 Subject: [PATCH 15/67] plugin: refactory and optimize pack task --- .../java/com/mcxiaoke/packer/cli/Packer.java | 19 +- gradle.properties | 2 +- .../com/mcxiaoke/packer/ng/Const.groovy | 34 +++ .../mcxiaoke/packer/ng/GradleExtension.groovy | 40 +--- .../mcxiaoke/packer/ng/GradlePlugin.groovy | 16 +- .../com/mcxiaoke/packer/ng/GradleTask.groovy | 199 +++++++++++------- sample/build.gradle | 70 ++---- 7 files changed, 204 insertions(+), 176 deletions(-) create mode 100644 plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Packer.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Packer.java index 8a3ac7d..9f91782 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/Packer.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Packer.java @@ -17,26 +17,25 @@ */ public class Packer { - public static void writeChannel(File apkFile, String channel) throws IOException { - PackerParser.create(apkFile).writeChannel(channel); + public static void writeChannel(File file, String channel) throws IOException { + PackerParser.create(file).writeChannel(channel); } - public static String readChannel(File apkFile) throws IOException { - return PackerParser.create(apkFile).readChannel(); + public static String readChannel(File file) throws IOException { + return PackerParser.create(file).readChannel(); } - public static boolean verifyChannel(File apkFile, String channel) throws IOException { - return verifyApk(apkFile) && (channel.equals(readChannel(apkFile))); + public static boolean verifyChannel(File file, String channel) throws IOException { + return verifyApk(file) && (channel.equals(readChannel(file))); } - public static boolean verifyApk(File apkFile) throws IOException { - ApkVerifier verifier = new Builder(apkFile).build(); + public static boolean verifyApk(File file) throws IOException { + ApkVerifier verifier = new Builder(file).build(); try { Result result = verifier.verify(); return result.isVerified() && result.isVerifiedUsingV1Scheme() - && result.isVerifiedUsingV2Scheme() - && !result.containsErrors(); + && result.isVerifiedUsingV2Scheme(); } catch (ApkFormatException e) { throw new IOException(e); } catch (NoSuchAlgorithmException e) { diff --git a/gradle.properties b/gradle.properties index f97e734..40611be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.0.13-SNAPSHOT +VERSION_NAME=1.0.21-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy new file mode 100644 index 0000000..cd36d68 --- /dev/null +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy @@ -0,0 +1,34 @@ +package com.mcxiaoke.packer.ng + +/** + * User: mcxiaoke + * Date: 2017/6/2 + * Time: 12:02 + */ +class Const { + static final String PROP_CHANNELS = "channels" + static final String PROP_OUTPUT = "output" + static final String PROP_FORMAT = "format" + + static final String DEFAULT_OUTPUT = "archives" // in build dir + + /* + * file name template string + * + * Available vars: + * 1. projectName + * 2. appName + * 3. appPkg + * 4. buildType + * 5. channel + * 6. versionName + * 7. versionCode + * 8. buildTime + * 9. fileMD5 + * 10. fileSHA1 + * + * default value: '${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}' + */ + static final String DEFAULT_FORMAT = + '${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}' +} diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy index 2eba3af..55ecad4 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy @@ -1,42 +1,18 @@ package com.mcxiaoke.packer.ng -import org.gradle.api.Project - -// Android Packer Plugin Extension class GradleExtension { - static final String DEFAULT_NAME_TEMPLATE = '${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}' - File archiveOutput - - /** - * file name template string - * - * Available vars: - * 1. projectName - * 2. appName - * 3. appPkg - * 4. buildType - * 5. channel - * 6. versionName - * 7. versionCode - * 8. buildTime - * 9. fileMD5 - * 10. fileSHA1 - * - * default value: '${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}' - */ String archiveNameFormat - List channelList; - File channelFile; - GradleExtension(Project project) { - archiveOutput = new File(project.rootProject.buildDir, "archives") - archiveNameFormat = DEFAULT_NAME_TEMPLATE - channelList = null - channelFile = null + @Override + String toString() { + return "{" + + "archiveOutput=" + archiveOutput + + "\narchiveNameFormat='" + archiveNameFormat + '\'' + + "\nchannelList=" + channelList + + "\nchannelFile=" + channelFile + + '}'; } - - } diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy index 320adb2..24995e9 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy @@ -20,24 +20,24 @@ class GradlePlugin implements Plugin { "the android plugin must be applied", null) } project.configurations.create(PLUGIN_NAME).extendsFrom(project.configurations.compile) - project.extensions.create(PLUGIN_NAME, GradleExtension, project) + project.extensions.create(PLUGIN_NAME, GradleExtension) project.afterEvaluate { project.android.applicationVariants.all { BaseVariant variant -> - addPackTask(variant) + addTasks(variant) } } } - void addPackTask(BaseVariant v) { - debug("addPackTask() for ${v.name}") - def packTask = project.task("apk${v.name.capitalize()}", + void addTasks(BaseVariant vt) { + debug("addPackTask() for ${vt.name}") + def variantTask = project.task("apk${vt.name.capitalize()}", type: GradleTask) { - variant = v + variant = vt extension = project.packer - dependsOn v.assemble + dependsOn vt.assemble } - debug("addPackTask() new variant task:${packTask.name}") + debug("addPackTask() new variant task:${variantTask.name}") def buildTypeName = v.buildType.name if (v.name != buildTypeName) { diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy index e89f5e3..976dcc6 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy @@ -4,13 +4,14 @@ import com.android.build.gradle.api.BaseVariant import com.mcxiaoke.packer.cli.Packer import groovy.io.FileType import groovy.text.SimpleTemplateEngine +import groovy.text.Template import org.gradle.api.DefaultTask import org.gradle.api.GradleException -import org.gradle.api.InvalidUserDataException import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction import java.text.SimpleDateFormat +import java.util.regex.Pattern /** * User: mcxiaoke @@ -26,50 +27,93 @@ class GradleTask extends DefaultTask { GradleExtension extension GradleTask() { - setDescription('pack original apk file and move to output dir') + description = 'add channel info to original APK file' } - @TaskAction - void showMessage() { - project.logger.info("${name}: ${description}") - } - - void checkChannels(List channels) throws GradleException { - if (channels == null || channels.isEmpty()) { - throw new InvalidUserDataException(":${name} " + - "no channels found, please check your market file!") + Template getNameTemplate() { + String format + String propValue = project.findProperty(Const.PROP_OUTPUT) + if (propValue != null) { + format = propValue.toString() + } else { + format = extension.archiveNameFormat } + if (format == null || format.isEmpty()) { + format = Const.DEFAULT_FORMAT + } + def engine = new SimpleTemplateEngine() + return engine.createTemplate(format) } - void checkSignature(File file) throws GradleException { + File getOriginalApkWithCheck() { + File file = variant.outputs[0].outputFile boolean apkVerified = Packer.verifyApk(file) if (!apkVerified) { - throw new GradleException(":${name} " + - "apk ${file} not v2 signed, please check your signingConfig!") + throw new GradleException("APK Signature Scheme v2 not verified: '${file}'") + } + return file + } + + File getOutputWithCheck() { + File outputDir + String propValue = project.findProperty(Const.PROP_OUTPUT) + if (propValue != null) { + String dirName = propValue.toString() + outputDir = new File(project.rootDir, dirName) + } else { + outputDir = extension.archiveOutput + } + if (outputDir == null) { + outputDir = new File(project.buildDir, Const.DEFAULT_OUTPUT) + } + if (!outputDir.exists()) { + outputDir.mkdirs() + } else { + logger.info(":${name} delete old APKs in ${outputDir.absolutePath}") + // delete old APKs + outputDir.eachFile(FileType.FILES) { file -> + if (file.getName().endsWith(".apk")) { + file.delete() + } + } } + return outputDir } - List getChannels() { + Set getChannelsWithCheck() { // -P channels=ch1,ch2,ch3 // -P channels=@channels.txt // channelList = [ch1,ch2,ch3] // channelFile = project.file("channels.txt") List channels = [] - if (project.hasProperty("channels")) { - def pv = project.property("channels").toString(); - logger.info(":${project.name} channels property: ${pv}") - if (pv.startsWith("@")) { - def fp = pv.substring(1) - if (fp != null) { - File f = new File(project.rootDir, fp) + // check command line property + def propValue = project.findProperty(Const.PROP_CHANNELS) + if (propValue != null) { + String prop = propValue.toString() + logger.info(":${project.name} channels property: '${prop}'") + if (prop.startsWith("@")) { + def fileName = prop.substring(1) + if (fileName != null) { + File f = new File(project.rootDir, fileName) + if (!f.isFile() || !f.canRead()) { + throw new GradleException("channel file not exists: '${f.absolutePath}'") + } channels = readChannels(f) + } else { + throw new GradleException("invalid channels property: '${prop}'") } } else { - channels = pv.split(",") + channels = prop.split(",") + } + if (channels == null || channels.isEmpty()) { + throw new GradleException("invalid channels property: '${prop}'") } - } else if (extension.channelList != null) { + return escape(channels) + } + // check extension property + if (extension.channelList != null) { channels = extension.channelList - logger.info(":${project.name} ext.channelList: ${extension.channelList}") + logger.info(":${project.name} ext.channelList: ${channels}") } else { File f; if (extension.channelFile != null) { @@ -78,80 +122,65 @@ class GradleTask extends DefaultTask { f = new File(project.rootDir, "channels.txt") } logger.info(":${project.name} extension.channelFile: ${f}") + if (!f.isFile() || !f.canRead()) { + throw new GradleException("channel file not exists: '${f.absolutePath}'") + } channels = readChannels(f) } - if (channels == null) { - channels = [] + if (channels == null || channels.isEmpty()) { + throw new GradleException("channels is null or empty") } - return channels + return escape(channels) } - List readChannels(File file) { - List channels = [] - file.eachLine { line, number -> - String[] parts = line.split('#') - if (parts && parts[0]) { - def c = parts[0].trim() - if (c) { - channels.add(c) - } - } else { - logger.info(":${project.name} skip invalid #${number}:'${line}'") - } - } - return channels + + void showProperties() { + println("Extension: ${extension}") + println("Property: ${Const.PROP_CHANNELS} = ${project.findProperty(Const.PROP_CHANNELS)}") + println("Property: ${Const.PROP_OUTPUT} = ${project.findProperty(Const.PROP_OUTPUT)}") + println("Property: ${Const.PROP_FORMAT} = ${project.findProperty(Const.PROP_FORMAT)}") } @TaskAction void pack() { - logger.info("====================PACKER NG TASK BEGIN====================") - File originalFile = variant.outputs[0].outputFile - checkSignature(originalFile) - List channels = getChannels(); - checkChannels(channels) - File outputDir = extension.archiveOutput - File apkPath = project.rootDir.toPath().relativize(originalFile.toPath()).toFile() - println(":${project.name}:${name} apk: ${apkPath}") - logger.info(":${name} output dir:${outputDir.absolutePath}") - if (!outputDir.exists()) { - outputDir.mkdirs() - } else { - logger.info(":${name} delete old apks in ${outputDir.absolutePath}") - // delete old APKs - outputDir.eachFile(FileType.FILES) { file -> - if (file.getName().endsWith(".apk")) { - file.delete() - } - } - } - println(":${project.name}:${name} channels:[${channels.join(', ')}]") + + println("=======================================================") + println("PackerNg - https://github.com/mcxiaoke/packer-ng-plugin") + println("=======================================================") + showProperties() + File apkFile = getOriginalApkWithCheck() + File outputDir = getOutputWithCheck() + Collection channels = getChannelsWithCheck() + Template template = getNameTemplate() + println("Input: ${apkFile.absolutePath}") + println("Output: ${outputDir.absolutePath}") + println("Channels: [${channels.join(', ')}]") for (String channel : channels) { File tempFile = new File(outputDir, channel + ".tmp") - copyTo(originalFile, tempFile) + copyTo(apkFile, tempFile) try { Packer.writeChannel(tempFile, channel) - String apkName = buildApkName(variant, channel, tempFile) + String apkName = buildApkName(channel, tempFile, template) File finalFile = new File(outputDir, apkName) if (Packer.verifyChannel(tempFile, channel)) { - println(":${project.name}:${name} Generating apk for ${channel}") + println("Generating apk: ${apkName} ......") tempFile.renameTo(finalFile) } else { - throw new GradleException(":${name} ${channel} apk verify failed.") + throw new GradleException("${channel} APK verify failed.") } } catch (IOException ex) { - throw new GradleException(":${name} ${channel} apk generate failed.", ex) + throw new GradleException("${channel} APK generate failed.", ex) } finally { tempFile.delete() } } - println(":${project.name}:${name} all ${channels.size()} apks saved to ${outputDir.path}") - println("\nPackerNg Build Successful!") - logger.info("====================PACKER NG TASK END====================") + println("Outputs:${outputDir.absolutePath}") + println("PackerNg Task Successful!") + println("=======================================================") } - String buildApkName(variant, channel, apkFile) { + String buildApkName(channel, file, template) { def buildTime = new SimpleDateFormat('yyyyMMdd-HHmmss').format(new Date()) - File file = apkFile def fileSHA1 = HASH.sha1(file) def nameMap = [ 'appName' : project.name, @@ -164,12 +193,26 @@ class GradleTask extends DefaultTask { 'appPkg' : variant.applicationId, 'buildTime' : buildTime ] + return template.make(nameMap).toString() + '.apk' + } - def dt = GradleExtension.DEFAULT_NAME_TEMPLATE - def engine = new SimpleTemplateEngine() - def template = extension.archiveNameFormat == null ? dt : extension.archiveNameFormat - def fileName = engine.createTemplate(template).make(nameMap).toString() - return fileName + '.apk' + static Set escape(Collection cs) { + Pattern pattern = ~/[\/:*?"'<>|]/ + return cs.collect { it.replaceAll(pattern, "_") }.toSet() + } + + static List readChannels(File file) { + List channels = [] + file.eachLine { line, number -> + String[] parts = line.split('#') + if (parts && parts[0]) { + def c = parts[0].trim() + if (c) { + channels.add(c) + } + } + } + return channels } static void copyTo(File src, File dest) { diff --git a/sample/build.gradle b/sample/build.gradle index 8b0e9ed..9ac53be 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.0.13-SNAPSHOT' + ext.packer_version = '1.0.21-SNAPSHOT' repositories { maven { url '/tmp/repo/' } @@ -35,12 +35,15 @@ dependencies { } } +//packer-begin packer { archiveNameFormat = '${channel}-${buildType}-v${versionName}-${versionCode}' archiveOutput = new File(project.rootProject.buildDir, "myapks") channelFile = new File(project.rootDir, "markets.txt") - channelList = ['Douban', 'Google', '中文市场', 'Some Market', '!@#$%^', '20070601'] + channelList = ['Doub!@#$%an', 'Google/', '中文/@#市场', + '!@#:?>;,.$%^', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] } +//packer-end android { @@ -61,7 +64,7 @@ android { } signingConfigs { - release { + releasev2 { storeFile file("android.keystore") storePassword "android" keyAlias "android" @@ -69,37 +72,43 @@ android { v2SigningEnabled true } - v2dev { + releasev1 { storeFile file("android.keystore") storePassword "android" keyAlias "android" keyPassword "android" - v2SigningEnabled true + v2SigningEnabled false } + } buildTypes { release { - signingConfig signingConfigs.release + signingConfig signingConfigs.releasev2 minifyEnabled false } - //someType { - // minifyEnabled false - // debuggable true - //} - beta { - signingConfig signingConfigs.release + releasev1 { + signingConfig signingConfigs.releasev1 minifyEnabled false - debuggable true } nosign { - // invalid: no signingConfig minifyEnabled false } + } + + productFlavors { + Dog {} + Cat {} + + Rabbit {} + + Fish { + signingConfig signingConfigs.releasev1 + } } lintOptions { @@ -113,36 +122,3 @@ android { } } - -//android.applicationVariants.all { variant -> -// variant.outputs.each { -// println("release: ${it.outputFile}") -// } -//} - -// gradle hook -//android.applicationVariants.all { variant -> -// def copyAssets = project.task("copy${variant.name.capitalize()}Assets", -// type: Copy) { -// from "~/Downloads/demo" -// into "src/main/assets/demo" -// doLast { -// println("copy assets to src/main/assets/demo for ${variant.name}") -// } -// } -// def deleteAssets = project.task("delete${variant.name.capitalize()}Assets", -// type: Delete) { -// delete "src/main/assets/demo" -// doLast { -// println("delete assets in src/main/assets/demo for ${variant.name}") -// } -// } -// variant.mergeAssets.dependsOn copyAssets -// variant.assemble.dependsOn deleteAssets -//} - -// gradle hook -//gradle.buildFinished { buildResult -> -// println "BUILD FINISHED" -// println "build failure - " + buildResult.failure -//} From e0a88815f7e39ad64ccce8e91034a2d8f265874a Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Mon, 5 Jun 2017 16:02:10 +0800 Subject: [PATCH 16/67] plugin: code fix and optimize --- .../packer/cli/{Packer.java => Bridge.java} | 0 .../common/{PackerParser.java => Parser.java} | 0 .../{PayloadUtils.java => Payload.java} | 0 .../com/mcxiaoke/packer/ng/Const.groovy | 1 + .../mcxiaoke/packer/ng/GradlePlugin.groovy | 43 +- .../com/mcxiaoke/packer/ng/GradleTask.groovy | 40 +- .../mcxiaoke/packer/ng/PluginException.groovy | 22 + .../com/mcxiaoke/packer/ng/StringVersion.java | 449 ++++++++++++++++++ 8 files changed, 525 insertions(+), 30 deletions(-) rename cli/src/main/java/com/mcxiaoke/packer/cli/{Packer.java => Bridge.java} (100%) rename common/src/main/java/com/mcxiaoke/packer/common/{PackerParser.java => Parser.java} (100%) rename common/src/main/java/com/mcxiaoke/packer/common/{PayloadUtils.java => Payload.java} (100%) create mode 100644 plugin/src/main/groovy/com/mcxiaoke/packer/ng/PluginException.groovy create mode 100644 plugin/src/main/java/com/mcxiaoke/packer/ng/StringVersion.java diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Packer.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java similarity index 100% rename from cli/src/main/java/com/mcxiaoke/packer/cli/Packer.java rename to cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java diff --git a/common/src/main/java/com/mcxiaoke/packer/common/PackerParser.java b/common/src/main/java/com/mcxiaoke/packer/common/Parser.java similarity index 100% rename from common/src/main/java/com/mcxiaoke/packer/common/PackerParser.java rename to common/src/main/java/com/mcxiaoke/packer/common/Parser.java diff --git a/common/src/main/java/com/mcxiaoke/packer/common/PayloadUtils.java b/common/src/main/java/com/mcxiaoke/packer/common/Payload.java similarity index 100% rename from common/src/main/java/com/mcxiaoke/packer/common/PayloadUtils.java rename to common/src/main/java/com/mcxiaoke/packer/common/Payload.java diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy index cd36d68..13d82bb 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy @@ -6,6 +6,7 @@ package com.mcxiaoke.packer.ng * Time: 12:02 */ class Const { + static final String HOME_PAGE = "https://github.com/mcxiaoke/packer-ng-plugin/" static final String PROP_CHANNELS = "channels" static final String PROP_OUTPUT = "output" static final String PROP_FORMAT = "format" diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy index 24995e9..0c687f7 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy @@ -1,9 +1,9 @@ package com.mcxiaoke.packer.ng import com.android.build.gradle.api.BaseVariant +import com.android.builder.Version import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.ProjectConfigurationException // Android PackerNg Plugin Source class GradlePlugin implements Plugin { @@ -16,8 +16,13 @@ class GradlePlugin implements Plugin { void apply(Project project) { this.project = project if (!project.plugins.hasPlugin("com.android.application")) { - throw new ProjectConfigurationException( - "the android plugin must be applied", null) + throw new PluginException( + "'com.android.application' plugin must be applied", null) + } + if (new StringVersion(Version.ANDROID_GRADLE_PLUGIN_VERSION) + < new StringVersion("2.2.0")) { + throw new PluginException( + "'com.android.tools.build:gradle' must be v2.2.0 or above", null) } project.configurations.create(PLUGIN_NAME).extendsFrom(project.configurations.compile) project.extensions.create(PLUGIN_NAME, GradleExtension) @@ -28,26 +33,42 @@ class GradlePlugin implements Plugin { } } + static boolean isV2SigningEnabled(BaseVariant vt) { + boolean e1 = false + boolean e2 = false + def s1 = vt.buildType.signingConfig + if (s1 && s1.signingReady) { + e1 = s1.v2SigningEnabled + } + def s2 = vt.mergedFlavor.signingConfig + if (s2 && s2.signingReady) { + e2 = s2.v2SigningEnabled + } + return e1 || e2 + } + void addTasks(BaseVariant vt) { - debug("addPackTask() for ${vt.name}") - def variantTask = project.task("apk${vt.name.capitalize()}", + debug("addTasks() for ${vt.name}") + def variantTask = project.task("generate${vt.name.capitalize()}Channels", type: GradleTask) { variant = vt extension = project.packer dependsOn vt.assemble } - debug("addPackTask() new variant task:${variantTask.name}") + debug("addTasks() new variant task:${variantTask.name}") - def buildTypeName = v.buildType.name - if (v.name != buildTypeName) { - def taskName = "apk${buildTypeName.capitalize()}" + def buildTypeName = vt.buildType.name + if (vt.name != buildTypeName) { + def taskName = "generate${buildTypeName.capitalize()}Channels" def task = project.tasks.findByName(taskName) if (task == null) { - debug("addPackTask() new build type task:${taskName}") - project.task(taskName, dependsOn: packTask) + task = project.task(taskName) } + task.dependsOn(variantTask) + debug("addTasks() build type task ${taskName}") } + } void debug(String msg) { diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy index 976dcc6..91f324c 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy @@ -1,12 +1,11 @@ package com.mcxiaoke.packer.ng import com.android.build.gradle.api.BaseVariant -import com.mcxiaoke.packer.cli.Packer +import com.mcxiaoke.packer.cli.Bridge import groovy.io.FileType import groovy.text.SimpleTemplateEngine import groovy.text.Template import org.gradle.api.DefaultTask -import org.gradle.api.GradleException import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction @@ -27,7 +26,7 @@ class GradleTask extends DefaultTask { GradleExtension extension GradleTask() { - description = 'add channel info to original APK file' + description = 'generate APK with channel info' } Template getNameTemplate() { @@ -47,9 +46,8 @@ class GradleTask extends DefaultTask { File getOriginalApkWithCheck() { File file = variant.outputs[0].outputFile - boolean apkVerified = Packer.verifyApk(file) - if (!apkVerified) { - throw new GradleException("APK Signature Scheme v2 not verified: '${file}'") + if (!Bridge.verifyApk(file)) { + throw new PluginException("APK Signature Scheme v2 verify failed: '${file}'") } return file } @@ -66,6 +64,10 @@ class GradleTask extends DefaultTask { if (outputDir == null) { outputDir = new File(project.buildDir, Const.DEFAULT_OUTPUT) } + String flavorName = variant.flavorName + if (flavorName.length() > 0) { + outputDir = new File(outputDir, flavorName) + } if (!outputDir.exists()) { outputDir.mkdirs() } else { @@ -96,17 +98,17 @@ class GradleTask extends DefaultTask { if (fileName != null) { File f = new File(project.rootDir, fileName) if (!f.isFile() || !f.canRead()) { - throw new GradleException("channel file not exists: '${f.absolutePath}'") + throw new PluginException("channel file not exists: '${f.absolutePath}'") } channels = readChannels(f) } else { - throw new GradleException("invalid channels property: '${prop}'") + throw new PluginException("invalid channels property: '${prop}'") } } else { channels = prop.split(",") } if (channels == null || channels.isEmpty()) { - throw new GradleException("invalid channels property: '${prop}'") + throw new PluginException("invalid channels property: '${prop}'") } return escape(channels) } @@ -123,12 +125,12 @@ class GradleTask extends DefaultTask { } logger.info(":${project.name} extension.channelFile: ${f}") if (!f.isFile() || !f.canRead()) { - throw new GradleException("channel file not exists: '${f.absolutePath}'") + throw new PluginException("channel file not exists: '${f.absolutePath}'") } channels = readChannels(f) } if (channels == null || channels.isEmpty()) { - throw new GradleException("channels is null or empty") + throw new PluginException("channels is null or empty") } return escape(channels) } @@ -142,12 +144,12 @@ class GradleTask extends DefaultTask { } @TaskAction - void pack() { + void generate() { println("=======================================================") println("PackerNg - https://github.com/mcxiaoke/packer-ng-plugin") println("=======================================================") - showProperties() +// showProperties() File apkFile = getOriginalApkWithCheck() File outputDir = getOutputWithCheck() Collection channels = getChannelsWithCheck() @@ -159,23 +161,22 @@ class GradleTask extends DefaultTask { File tempFile = new File(outputDir, channel + ".tmp") copyTo(apkFile, tempFile) try { - Packer.writeChannel(tempFile, channel) + Bridge.writeChannel(tempFile, channel) String apkName = buildApkName(channel, tempFile, template) File finalFile = new File(outputDir, apkName) - if (Packer.verifyChannel(tempFile, channel)) { - println("Generating apk: ${apkName} ......") + if (Bridge.verifyChannel(tempFile, channel)) { + println("Generating: ${apkName}") tempFile.renameTo(finalFile) } else { - throw new GradleException("${channel} APK verify failed.") + throw new PluginException("${channel} APK verify failed") } } catch (IOException ex) { - throw new GradleException("${channel} APK generate failed.", ex) + throw new PluginException("${channel} APK generate failed", ex) } finally { tempFile.delete() } } println("Outputs:${outputDir.absolutePath}") - println("PackerNg Task Successful!") println("=======================================================") } @@ -187,6 +188,7 @@ class GradleTask extends DefaultTask { 'projectName': project.rootProject.name, 'fileSHA1' : fileSHA1, 'channel' : channel, + 'flavor' : variant.flavorName, 'buildType' : variant.buildType.name, 'versionName': variant.versionName, 'versionCode': variant.versionCode, diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PluginException.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PluginException.groovy new file mode 100644 index 0000000..937f5f8 --- /dev/null +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PluginException.groovy @@ -0,0 +1,22 @@ +package com.mcxiaoke.packer.ng + +import org.gradle.api.GradleException + +/** + * User: mcxiaoke + * Date: 2017/6/5 + * Time: 15:29 + */ +class PluginException extends GradleException { + PluginException() { + super("See docs on ${Const.HOME_PAGE}") + } + + PluginException(final String message) { + super(message + ", See docs on ${Const.HOME_PAGE}") + } + + PluginException(final String message, final Throwable cause) { + super(message + ", See docs on ${Const.HOME_PAGE}", cause) + } +} diff --git a/plugin/src/main/java/com/mcxiaoke/packer/ng/StringVersion.java b/plugin/src/main/java/com/mcxiaoke/packer/ng/StringVersion.java new file mode 100644 index 0000000..ac23f82 --- /dev/null +++ b/plugin/src/main/java/com/mcxiaoke/packer/ng/StringVersion.java @@ -0,0 +1,449 @@ +package com.mcxiaoke.packer.ng; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Stack; + +/** + *

+ * Generic implementation of version comparison. + *

+ * + * Features: + *
    + *
  • mixing of '-' (hyphen) and '.' (dot) separators,
  • + *
  • transition between characters and digits also constitutes a separator: + * 1.0alpha1 => [1, 0, alpha, 1]
  • + *
  • unlimited number of version components,
  • + *
  • version components in the text can be digits or strings,
  • + *
  • strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering. + * Well-known qualifiers (case insensitive) are:
      + *
    • alpha or a
    • + *
    • beta or b
    • + *
    • milestone or m
    • + *
    • rc or cr
    • + *
    • snapshot
    • + *
    • (the empty string) or ga or final
    • + *
    • sp
    • + *
    + * Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive), + *
  • + *
  • a hyphen usually precedes a qualifier, and is always less important than something preceded with a dot.
  • + *
+ * + * @author Kenney Westerhof + * @author Hervé Boutemy + * @see "Versioning" on Maven Wiki + */ +class StringVersion + implements Comparable { + private String value; + + private String canonical; + + private ListItem items; + + private interface Item { + int INTEGER_ITEM = 0; + int STRING_ITEM = 1; + int LIST_ITEM = 2; + + int compareTo(Item item); + + int getType(); + + boolean isNull(); + } + + /** + * Represents a numeric item in the version item list. + */ + private static class IntegerItem + implements Item { + private static final BigInteger BIG_INTEGER_ZERO = new BigInteger("0"); + + private final BigInteger value; + + public static final IntegerItem ZERO = new IntegerItem(); + + private IntegerItem() { + this.value = BIG_INTEGER_ZERO; + } + + public IntegerItem(String str) { + this.value = new BigInteger(str); + } + + public int getType() { + return INTEGER_ITEM; + } + + public boolean isNull() { + return BIG_INTEGER_ZERO.equals(value); + } + + public int compareTo(Item item) { + if (item == null) { + return BIG_INTEGER_ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 > 1 + } + + switch (item.getType()) { + case INTEGER_ITEM: + return value.compareTo(((IntegerItem) item).value); + + case STRING_ITEM: + return 1; // 1.1 > 1-sp + + case LIST_ITEM: + return 1; // 1.1 > 1-1 + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + return value.toString(); + } + } + + /** + * Represents a string in the version item list, usually a qualifier. + */ + private static class StringItem + implements Item { + private static final String[] QUALIFIERS = {"alpha", "beta", "milestone", "rc", "snapshot", "", "sp"}; + + @SuppressWarnings("checkstyle:constantname") + private static final List _QUALIFIERS = Arrays.asList(QUALIFIERS); + + private static final Properties ALIASES = new Properties(); + + static { + ALIASES.put("ga", ""); + ALIASES.put("final", ""); + ALIASES.put("cr", "rc"); + } + + /** + * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes + * the version older than one without a qualifier, or more recent. + */ + private static final String RELEASE_VERSION_INDEX = String.valueOf(_QUALIFIERS.indexOf("")); + + private String value; + + public StringItem(String value, boolean followedByDigit) { + if (followedByDigit && value.length() == 1) { + // a1 = alpha-1, b1 = beta-1, m1 = milestone-1 + switch (value.charAt(0)) { + case 'a': + value = "alpha"; + break; + case 'b': + value = "beta"; + break; + case 'm': + value = "milestone"; + break; + default: + } + } + this.value = ALIASES.getProperty(value, value); + } + + public int getType() { + return STRING_ITEM; + } + + public boolean isNull() { + return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0); + } + + /** + * Returns a comparable value for a qualifier. + * + * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical + * ordering. + * + * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1 + * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character, + * so this is still fast. If more characters are needed then it requires a lexical sort anyway. + * + * @param qualifier + * @return an equivalent value that can be used with lexical comparison + */ + public static String comparableQualifier(String qualifier) { + int i = _QUALIFIERS.indexOf(qualifier); + + return i == -1 ? (_QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i); + } + + public int compareTo(Item item) { + if (item == null) { + // 1-rc < 1, 1-ga > 1 + return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX); + } + switch (item.getType()) { + case INTEGER_ITEM: + return -1; // 1.any < 1.1 ? + + case STRING_ITEM: + return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value)); + + case LIST_ITEM: + return -1; // 1.any < 1-1 + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + return value; + } + } + + /** + * Represents a version list item. This class is used both for the global item list and for sub-lists (which start + * with '-(number)' in the version specification). + */ + private static class ListItem + extends ArrayList + implements Item { + public int getType() { + return LIST_ITEM; + } + + public boolean isNull() { + return (size() == 0); + } + + void normalize() { + for (int i = size() - 1; i >= 0; i--) { + Item lastItem = get(i); + + if (lastItem.isNull()) { + // remove null trailing items: 0, "", empty list + remove(i); + } else if (!(lastItem instanceof ListItem)) { + break; + } + } + } + + public int compareTo(Item item) { + if (item == null) { + if (size() == 0) { + return 0; // 1-0 = 1- (normalize) = 1 + } + Item first = get(0); + return first.compareTo(null); + } + switch (item.getType()) { + case INTEGER_ITEM: + return -1; // 1-1 < 1.0.x + + case STRING_ITEM: + return 1; // 1-1 > 1-sp + + case LIST_ITEM: + Iterator left = iterator(); + Iterator right = ((ListItem) item).iterator(); + + while (left.hasNext() || right.hasNext()) { + Item l = left.hasNext() ? left.next() : null; + Item r = right.hasNext() ? right.next() : null; + + // if this is shorter, then invert the compare and mul with -1 + int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r); + + if (result != 0) { + return result; + } + } + + return 0; + + default: + throw new RuntimeException("invalid item: " + item.getClass()); + } + } + + public String toString() { + StringBuilder buffer = new StringBuilder(); + for (Item item : this) { + if (buffer.length() > 0) { + buffer.append((item instanceof ListItem) ? '-' : '.'); + } + buffer.append(item); + } + return buffer.toString(); + } + } + + public StringVersion(String version) { + parseVersion(version); + } + + public final void parseVersion(String version) { + this.value = version; + + items = new ListItem(); + + version = version.toLowerCase(Locale.ENGLISH); + + ListItem list = items; + + Stack stack = new Stack<>(); + stack.push(list); + + boolean isDigit = false; + + int startIndex = 0; + + for (int i = 0; i < version.length(); i++) { + char c = version.charAt(i); + + if (c == '.') { + if (i == startIndex) { + list.add(IntegerItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + } else if (c == '-') { + if (i == startIndex) { + list.add(IntegerItem.ZERO); + } else { + list.add(parseItem(isDigit, version.substring(startIndex, i))); + } + startIndex = i + 1; + + list.add(list = new ListItem()); + stack.push(list); + } else if (Character.isDigit(c)) { + if (!isDigit && i > startIndex) { + list.add(new StringItem(version.substring(startIndex, i), true)); + startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); + } + + isDigit = true; + } else { + if (isDigit && i > startIndex) { + list.add(parseItem(true, version.substring(startIndex, i))); + startIndex = i; + + list.add(list = new ListItem()); + stack.push(list); + } + + isDigit = false; + } + } + + if (version.length() > startIndex) { + list.add(parseItem(isDigit, version.substring(startIndex))); + } + + while (!stack.isEmpty()) { + list = (ListItem) stack.pop(); + list.normalize(); + } + + canonical = items.toString(); + } + + private static Item parseItem(boolean isDigit, String buf) { + return isDigit ? new IntegerItem(buf) : new StringItem(buf, false); + } + + public int compareTo(StringVersion o) { + return items.compareTo(o.items); + } + + public String toString() { + return value; + } + + public String getCanonical() { + return canonical; + } + + public boolean equals(Object o) { + return (o instanceof StringVersion) && canonical.equals(((StringVersion) o).canonical); + } + + public int hashCode() { + return canonical.hashCode(); + } + + // CHECKSTYLE_OFF: LineLength + + /** + * Main to test version parsing and comparison. + *

+ * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue + *

java -jar ${maven.repo.local}/org/apache/maven/maven-artifact/${maven.version}/maven-artifact-${maven.version}.jar "1.2.7" "1.2-SNAPSHOT"
+ * command to command line. Result of given command will be something like this: + *
+     * Display parameters as parsed by Maven (in canonical form) and comparison result:
+     * 1. 1.2.7 == 1.2.7
+     *    1.2.7 > 1.2-SNAPSHOT
+     * 2. 1.2-SNAPSHOT == 1.2-snapshot
+     * 
+ * + * @param args the version strings to parse and compare. You can pass arbitrary number of version strings and always + * two adjacent will be compared + */ + // CHECKSTYLE_ON: LineLength + public static void main(String... args) { + System.out.println("Display parameters as parsed by Maven (in canonical form) and comparison result:"); + if (args.length == 0) { + return; + } + + StringVersion prev = null; + int i = 1; + for (String version : args) { + StringVersion c = new StringVersion(version); + + if (prev != null) { + int compare = prev.compareTo(c); + System.out.println(" " + prev.toString() + ' ' + + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">")) + ' ' + version); + } + + System.out.println(String.valueOf(i++) + ". " + version + " == " + c.getCanonical()); + + prev = c; + } + } +} From 6ce7b8301ad4686819f6bfa5be5ab153cef76c79 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Mon, 5 Jun 2017 16:02:27 +0800 Subject: [PATCH 17/67] rename classes, tweak docs --- .../java/com/mcxiaoke/packer/cli/Bridge.java | 8 ++--- .../java/com/mcxiaoke/packer/cli/Main.java | 29 ++++++++-------- .../com/mcxiaoke/packer/cli/help.txt | 4 +-- .../com/mcxiaoke/packer/common/Parser.java | 28 +++++++-------- .../com/mcxiaoke/packer/common/Payload.java | 2 +- .../mcxiaoke/packer/common/PayloadTests.java | 34 +++++++++---------- .../com/mcxiaoke/packer/helper/PackerNg.java | 4 +-- 7 files changed, 54 insertions(+), 55 deletions(-) diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java index 9f91782..e72e9d5 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java @@ -4,7 +4,7 @@ import com.android.apksig.ApkVerifier.Builder; import com.android.apksig.ApkVerifier.Result; import com.android.apksig.apk.ApkFormatException; -import com.mcxiaoke.packer.common.PackerParser; +import com.mcxiaoke.packer.common.Parser; import java.io.File; import java.io.IOException; @@ -15,14 +15,14 @@ * Date: 2017/5/26 * Time: 16:21 */ -public class Packer { +public class Bridge { public static void writeChannel(File file, String channel) throws IOException { - PackerParser.create(file).writeChannel(channel); + Parser.create(file).writeChannel(channel); } public static String readChannel(File file) throws IOException { - return PackerParser.create(file).readChannel(); + return Parser.create(file).readChannel(); } public static boolean verifyChannel(File file, String channel) throws IOException { diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java index 985d21a..99ae0ca 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java @@ -29,8 +29,8 @@ public static void main(String[] args) { final String cmd = args[0]; final String[] params = Arrays.copyOfRange(args, 1, args.length); try { - if ("pack".equals(cmd)) { - pack(params); + if ("generate".equals(cmd)) { + generate(params); } else if ("verify".equals(cmd)) { verify(params); } else if ("help".equals(cmd)) { @@ -52,7 +52,7 @@ public static void printUsage() { Helper.printUsage(); } - private static void pack(String[] params) throws Exception { + private static void generate(String[] params) throws Exception { if (params.length == 0) { printUsage(); return; @@ -91,9 +91,9 @@ private static void pack(String[] params) throws Exception { String value = optionsParser.getRequiredValue("Output Directory"); outputDir = new File(value); } else { - printUsage(); System.err.println( "Unsupported option: " + form); + printUsage(); } } params = optionsParser.getRemainingParams(); @@ -106,17 +106,17 @@ private static void pack(String[] params) throws Exception { if (outputDir == null) { outputDir = new File(OUTPUT); } - doPack(apkFile, channels, outputDir); + doGenerate(apkFile, channels, outputDir); } - private static void doPack(File apkFile, List channels, File outputDir) + private static void doGenerate(File apkFile, List channels, File outputDir) throws IOException { if (apkFile == null || !apkFile.exists() || !apkFile.isFile()) { throw new IOException("Invalid Input APK: " + apkFile); } - if (!Packer.verifyApk(apkFile)) { + if (!Bridge.verifyApk(apkFile)) { throw new IOException("Invalid Signature: " + apkFile); } if (outputDir.exists()) { @@ -124,9 +124,9 @@ private static void doPack(File apkFile, List channels, File outputDir) } else { outputDir.mkdirs(); } - System.out.println("File: " + apkFile.getAbsolutePath()); + System.out.println("Input: " + apkFile.getAbsolutePath()); + System.out.println("Output:" + outputDir.getAbsolutePath()); System.out.println("Channels:" + Arrays.toString(channels.toArray())); - System.out.println("OutputDir:" + outputDir.getAbsolutePath()); final String fileName = apkFile.getName(); final String baseName = Helper.getBaseName(fileName); final String extName = Helper.getExtName(fileName); @@ -135,15 +135,14 @@ private static void doPack(File apkFile, List channels, File outputDir) "%s-%s.%s", baseName, channel, extName); File destFile = new File(outputDir, apkName); Helper.copyFile(apkFile, destFile); - Packer.writeChannel(destFile, channel); - if (Packer.verifyChannel(destFile, channel)) { - System.out.println("Generating APK: " + apkName); + Bridge.writeChannel(destFile, channel); + if (Bridge.verifyChannel(destFile, channel)) { + System.out.println("Generating " + apkName); } else { destFile.delete(); throw new IOException("Failed to verify APK: " + apkName); } } - System.out.println("All APK files saved to " + outputDir.getAbsolutePath()); } private static void verify(String[] params) throws Exception { @@ -165,8 +164,8 @@ private static void doVerify(File apkFile) throws IOException { || !apkFile.isFile()) { throw new IOException("Invalid Input APK: " + apkFile); } - final boolean verified = Packer.verifyApk(apkFile); - final String channel = Packer.readChannel(apkFile); + final boolean verified = Bridge.verifyApk(apkFile); + final String channel = Bridge.readChannel(apkFile); System.out.println("File: " + apkFile); System.out.println("Signed:" + verified); System.out.println("Channel:" + channel); diff --git a/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt b/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt index 5cf110e..16f2e04 100644 --- a/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt +++ b/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt @@ -12,12 +12,12 @@ USAGE packer-ng [options] packer-ng --help [-h] - packer-ng pack --channels --output apk + packer-ng generate --channels --output apk packer-ng verify apk EXAMPLE -pack Add channel info to the provided APK +generate Add channel info to the provided APK packer-ng pack --channels=ch1,ch2,ch3 --output=archives app.apk packer-ng pack --channels=@file.txt --output=archives app.apk diff --git a/common/src/main/java/com/mcxiaoke/packer/common/Parser.java b/common/src/main/java/com/mcxiaoke/packer/common/Parser.java index 26fbd27..48af1c7 100644 --- a/common/src/main/java/com/mcxiaoke/packer/common/Parser.java +++ b/common/src/main/java/com/mcxiaoke/packer/common/Parser.java @@ -8,19 +8,19 @@ * Date: 2017/5/17 * Time: 15:39 */ -public class PackerParser { +public class Parser { - public static PackerParser create(File apkFile) { - return new PackerParser(apkFile); + public static Parser create(File apkFile) { + return new Parser(apkFile); } - public static PackerParser create(File apkFile, String channelKey) { - return new PackerParser(apkFile, channelKey); + public static Parser create(File apkFile, String channelKey) { + return new Parser(apkFile, channelKey); } - public static PackerParser create(File apkFile, String channelKey, int channelBlockId) { - return new PackerParser(apkFile, channelKey, channelBlockId); + public static Parser create(File apkFile, String channelKey, int channelBlockId) { + return new Parser(apkFile, channelKey, channelBlockId); } // channel info key @@ -33,28 +33,28 @@ public static PackerParser create(File apkFile, String channelKey, int channelBl private String channelKey; private int channelBlockId; - PackerParser(final File apkFile) { + Parser(final File apkFile) { this(apkFile, DEFAULT_CHANNEL_KEY, DEFAULT_CHANNEL_BLOCK_ID); } - PackerParser(final File apkFile, final String channelKey) { + Parser(final File apkFile, final String channelKey) { this(apkFile, channelKey, DEFAULT_CHANNEL_BLOCK_ID); } - PackerParser(final File apkFile, - final String channelKey, - final int channelBlockId) { + Parser(final File apkFile, + final String channelKey, + final int channelBlockId) { this.apkFile = apkFile; this.channelKey = channelKey; this.channelBlockId = channelBlockId; } public String readChannel() throws IOException { - return PayloadUtils.readChannel(apkFile, channelKey, channelBlockId); + return Payload.readChannel(apkFile, channelKey, channelBlockId); } public void writeChannel(final String channel) throws IOException { - PayloadUtils.writeChannel(apkFile, channel, channelKey, channelBlockId); + Payload.writeChannel(apkFile, channel, channelKey, channelBlockId); } diff --git a/common/src/main/java/com/mcxiaoke/packer/common/Payload.java b/common/src/main/java/com/mcxiaoke/packer/common/Payload.java index 9ad4538..583f34c 100644 --- a/common/src/main/java/com/mcxiaoke/packer/common/Payload.java +++ b/common/src/main/java/com/mcxiaoke/packer/common/Payload.java @@ -14,7 +14,7 @@ * Date: 2017/5/26 * Time: 13:18 */ -public class PayloadUtils { +public class Payload { // charset utf8 public static final String UTF8 = "UTF-8"; diff --git a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java index f06afb6..381a378 100644 --- a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java +++ b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java @@ -77,8 +77,8 @@ public void testOverrideSignature() throws IOException, NoSuchAlgorithmException { File f = newTestFile(); // don't write with APK Signature Scheme v2 Block ID 0x7109871a - PayloadUtils.writeRaw(f, "OverrideSignatureSchemeBlock", 0x7109871a); - assertEquals("OverrideSignatureSchemeBlock", PayloadUtils.readRaw(f, 0x7109871a)); + Payload.writeRaw(f, "OverrideSignatureSchemeBlock", 0x7109871a); + assertEquals("OverrideSignatureSchemeBlock", Payload.readRaw(f, 0x7109871a)); ApkVerifier verifier = new Builder(f).build(); Result result = verifier.verify(); final List errors = result.getErrors(); @@ -113,10 +113,10 @@ public void testBytesWrite2() throws IOException { public void testStringWrite() throws IOException { File f = newTestFile(); - PayloadUtils.writeRaw(f, "Test String", 0x717a786b); - assertEquals("Test String", PayloadUtils.readRaw(f, 0x717a786b)); - PayloadUtils.writeRaw(f, "中文和特殊符号测试!@#¥%……*()《》?:【】、", 0x717a786b); - assertEquals("中文和特殊符号测试!@#¥%……*()《》?:【】、", PayloadUtils.readRaw(f, 0x717a786b)); + Payload.writeRaw(f, "Test String", 0x717a786b); + assertEquals("Test String", Payload.readRaw(f, 0x717a786b)); + Payload.writeRaw(f, "中文和特殊符号测试!@#¥%……*()《》?:【】、", 0x717a786b); + assertEquals("中文和特殊符号测试!@#¥%……*()《》?:【】、", Payload.readRaw(f, 0x717a786b)); checkApkVerified(f); } @@ -127,8 +127,8 @@ public void testValuesWrite() throws IOException { in.put("名字", "哈哈啊哈哈哈"); in.put("!@#$!%^@&*()_+\"?:><", "渠道Google"); in.put("12345abcd", "2017"); - PayloadUtils.writeValues(f, in, 0x12345); - Map out = PayloadUtils.readValues(f, 0x12345); + Payload.writeValues(f, in, 0x12345); + Map out = Payload.readValues(f, 0x12345); assertNotNull(out); assertEquals(in.size(), out.size()); for (Map.Entry entry : in.entrySet()) { @@ -142,19 +142,19 @@ public void testValuesMixedWrite() throws IOException { Map in = new HashMap<>(); in.put("!@#$!%^@&*()_+\"?:><", "渠道Google"); in.put("12345abcd", "2017"); - PayloadUtils.writeValues(f, in, 0x123456); - PayloadUtils.writeChannel(f, "Mixed", "hello", 0x8888); - Map out = PayloadUtils.readValues(f, 0x123456); + Payload.writeValues(f, in, 0x123456); + Payload.writeChannel(f, "Mixed", "hello", 0x8888); + Map out = Payload.readValues(f, 0x123456); assertNotNull(out); assertEquals(in.size(), out.size()); for (Map.Entry entry : in.entrySet()) { assertEquals(entry.getValue(), out.get(entry.getKey())); } - assertEquals("Mixed", PayloadUtils.readChannel(f, "hello", 0x8888)); - PayloadUtils.writeRaw(f, "RawValue", 0x2017); - assertEquals("RawValue", PayloadUtils.readRaw(f, 0x2017)); - PayloadUtils.writeRaw(f, "OverrideValues", 0x123456); - assertEquals("OverrideValues", PayloadUtils.readRaw(f, 0x123456)); + assertEquals("Mixed", Payload.readChannel(f, "hello", 0x8888)); + Payload.writeRaw(f, "RawValue", 0x2017); + assertEquals("RawValue", Payload.readRaw(f, 0x2017)); + Payload.writeRaw(f, "OverrideValues", 0x123456); + assertEquals("OverrideValues", Payload.readRaw(f, 0x123456)); checkApkVerified(f); } @@ -218,7 +218,7 @@ public void testBufferWrite() throws IOException { public void testChannelWriteRead() throws IOException { File f = newTestFile(); - PackerParser p = new PackerParser(f); + Parser p = new Parser(f); p.writeChannel("Hello"); assertEquals("Hello", p.readChannel()); p.writeChannel("中文"); diff --git a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java index 817833e..82e806d 100644 --- a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java +++ b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java @@ -2,7 +2,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; -import com.mcxiaoke.packer.common.PackerParser; +import com.mcxiaoke.packer.common.Parser; import java.io.File; @@ -44,7 +44,7 @@ private static ChannelInfo getMarketInternal(final Context context, try { final ApplicationInfo info = context.getApplicationInfo(); final File apkFile = new File(info.sourceDir); - final PackerParser parser = PackerParser.create(apkFile); + final Parser parser = Parser.create(apkFile); market = parser.readChannel(); } catch (Exception e) { error = e; From ce8691b9d9ba7c5a78dc9952ce422cdd0b943de9 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Mon, 5 Jun 2017 16:04:39 +0800 Subject: [PATCH 18/67] update and fix sample --- gradle.properties | 2 +- sample/build.gradle | 26 ++++++++----------- .../mcxiaoke/packer/samples/MainActivity.java | 8 +++--- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/gradle.properties b/gradle.properties index 40611be..ef50805 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.0.21-SNAPSHOT +VERSION_NAME=1.4.0-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/sample/build.gradle b/sample/build.gradle index 9ac53be..8497c4b 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.0.21-SNAPSHOT' + ext.packer_version = '1.4.0-SNAPSHOT' repositories { maven { url '/tmp/repo/' } @@ -37,11 +37,11 @@ dependencies { //packer-begin packer { - archiveNameFormat = '${channel}-${buildType}-v${versionName}-${versionCode}' + archiveNameFormat = '${appPkg}-${buildType}-v${versionName}-${channel}' archiveOutput = new File(project.rootProject.buildDir, "myapks") channelFile = new File(project.rootDir, "markets.txt") - channelList = ['Doub!@#$%an', 'Google/', '中文/@#市场', - '!@#:?>;,.$%^', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] + channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', + 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] } //packer-end @@ -64,7 +64,7 @@ android { } signingConfigs { - releasev2 { + v2 { storeFile file("android.keystore") storePassword "android" keyAlias "android" @@ -72,7 +72,7 @@ android { v2SigningEnabled true } - releasev1 { + v1 { storeFile file("android.keystore") storePassword "android" keyAlias "android" @@ -84,16 +84,16 @@ android { buildTypes { release { - signingConfig signingConfigs.releasev2 + signingConfig signingConfigs.v2 minifyEnabled false } - releasev1 { - signingConfig signingConfigs.releasev1 + beta { + signingConfig signingConfigs.v1 minifyEnabled false } - nosign { + alpha { minifyEnabled false } @@ -104,11 +104,7 @@ android { Cat {} - Rabbit {} - - Fish { - signingConfig signingConfigs.releasev1 - } + Fish {} } lintOptions { diff --git a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java index 90ea60b..c0610a1 100644 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java +++ b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java @@ -14,6 +14,7 @@ import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.AppCompatActivity; import android.util.DisplayMetrics; import android.view.Display; import android.view.ViewGroup; @@ -24,6 +25,7 @@ import com.mcxiaoke.next.utils.AndroidUtils; import com.mcxiaoke.next.utils.LogUtils; import com.mcxiaoke.next.utils.StringUtils; +import com.mcxiaoke.packer.helper.PackerNg; import com.mcxiaoke.packer.samples.BuildConfig; import com.mcxiaoke.packer.samples.R; @@ -33,7 +35,7 @@ import java.util.Set; -public class MainActivity extends ActionBarActivity { +public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); @InjectView(R.id.container) @@ -63,8 +65,7 @@ private void addAppInfoSection() { StringBuilder builder = new StringBuilder(); builder.append("[AppInfo]\n"); builder.append("SourceDir: ").append(getSourceDir(this)).append("\n"); -// builder.append("Market: ").append(PackerNg.getChannel(this)).append("\n"); -// builder.append("MarketInfo: ").append(PackerNg.getChannel(this)).append("\n"); + builder.append("Market: ").append(PackerNg.getChannel(this)).append("\n"); builder.append("Name: ").append(getString(info.labelRes)).append("\n"); builder.append("Package: ").append(BuildConfig.APPLICATION_ID).append("\n"); builder.append("VersionCode: ").append(BuildConfig.VERSION_CODE).append("\n"); @@ -79,6 +80,7 @@ private void addAppInfoSection() { builder.append("\n"); addSection(builder.toString()); } catch (Exception e) { + e.printStackTrace(); } From bdbcbf748326fb0fcf9116b618e4c9f68aeaa3a4 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Mon, 5 Jun 2017 18:10:39 +0800 Subject: [PATCH 19/67] tweak generate , tweak logs --- gradle.properties | 2 +- .../mcxiaoke/packer/ng/GradleExtension.groovy | 2 + .../com/mcxiaoke/packer/ng/GradleTask.groovy | 49 +++++++++++-------- .../mcxiaoke/packer/ng/PluginException.groovy | 7 +-- sample/build.gradle | 17 ++++--- 5 files changed, 46 insertions(+), 31 deletions(-) diff --git a/gradle.properties b/gradle.properties index ef50805..d0ef227 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.4.0-SNAPSHOT +VERSION_NAME=1.5.3-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy index 55ecad4..05648b8 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy @@ -5,6 +5,7 @@ class GradleExtension { String archiveNameFormat List channelList; File channelFile; + Map channelMap; @Override String toString() { @@ -13,6 +14,7 @@ class GradleExtension { "\narchiveNameFormat='" + archiveNameFormat + '\'' + "\nchannelList=" + channelList + "\nchannelFile=" + channelFile + + "\nchannelMap=" + channelMap + '}'; } } diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy index 91f324c..f348787 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy @@ -112,19 +112,23 @@ class GradleTask extends DefaultTask { } return escape(channels) } - // check extension property if (extension.channelList != null) { channels = extension.channelList logger.info(":${project.name} ext.channelList: ${channels}") - } else { - File f; - if (extension.channelFile != null) { - f = extension.channelFile - } else { - f = new File(project.rootDir, "channels.txt") + } else if (extension.channelMap != null) { + String flavorName = variant.flavorName + File f = extension.channelMap.get(flavorName) + logger.info(":${project.name} extension.channelMap file: ${f}") + if (f == null || !f.isFile()) { + throw new PluginException("channel file not exists: '${f.absolutePath}'") } + if (f != null && f.isFile()) { + channels = readChannels(f) + } + } else if (extension.channelFile != null) { + File f = extension.channelFile logger.info(":${project.name} extension.channelFile: ${f}") - if (!f.isFile() || !f.canRead()) { + if (!f.isFile()) { throw new PluginException("channel file not exists: '${f.absolutePath}'") } channels = readChannels(f) @@ -137,25 +141,28 @@ class GradleTask extends DefaultTask { void showProperties() { - println("Extension: ${extension}") - println("Property: ${Const.PROP_CHANNELS} = ${project.findProperty(Const.PROP_CHANNELS)}") - println("Property: ${Const.PROP_OUTPUT} = ${project.findProperty(Const.PROP_OUTPUT)}") - println("Property: ${Const.PROP_FORMAT} = ${project.findProperty(Const.PROP_FORMAT)}") + logger.info("Extension: ${extension}") + logger.info("Property: ${Const.PROP_CHANNELS} = " + + "${project.findProperty(Const.PROP_CHANNELS)}") + logger.info("Property: ${Const.PROP_OUTPUT} = " + + "${project.findProperty(Const.PROP_OUTPUT)}") + logger.info("Property: ${Const.PROP_FORMAT} = " + + "${project.findProperty(Const.PROP_FORMAT)}") } @TaskAction void generate() { - - println("=======================================================") + println("============================================================") println("PackerNg - https://github.com/mcxiaoke/packer-ng-plugin") - println("=======================================================") -// showProperties() + println("============================================================") + showProperties() File apkFile = getOriginalApkWithCheck() File outputDir = getOutputWithCheck() Collection channels = getChannelsWithCheck() Template template = getNameTemplate() - println("Input: ${apkFile.absolutePath}") - println("Output: ${outputDir.absolutePath}") + println("Variant: ${variant.name}") + println("Input: ${apkFile.path}") + println("Output: ${outputDir.path}") println("Channels: [${channels.join(', ')}]") for (String channel : channels) { File tempFile = new File(outputDir, channel + ".tmp") @@ -165,7 +172,7 @@ class GradleTask extends DefaultTask { String apkName = buildApkName(channel, tempFile, template) File finalFile = new File(outputDir, apkName) if (Bridge.verifyChannel(tempFile, channel)) { - println("Generating: ${apkName}") + println("--> Generating: ${apkName}") tempFile.renameTo(finalFile) } else { throw new PluginException("${channel} APK verify failed") @@ -176,8 +183,8 @@ class GradleTask extends DefaultTask { tempFile.delete() } } - println("Outputs:${outputDir.absolutePath}") - println("=======================================================") + println("Outputs: ${outputDir.absolutePath}") + println("============================================================") } String buildApkName(channel, file, template) { diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PluginException.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PluginException.groovy index 937f5f8..a51b5da 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PluginException.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PluginException.groovy @@ -9,14 +9,15 @@ import org.gradle.api.GradleException */ class PluginException extends GradleException { PluginException() { - super("See docs on ${Const.HOME_PAGE}") +// super("See docs on ${Const.HOME_PAGE}") + super() } PluginException(final String message) { - super(message + ", See docs on ${Const.HOME_PAGE}") + super(message) } PluginException(final String message, final Throwable cause) { - super(message + ", See docs on ${Const.HOME_PAGE}", cause) + super(message, cause) } } diff --git a/sample/build.gradle b/sample/build.gradle index 8497c4b..f776579 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.4.0-SNAPSHOT' + ext.packer_version = '1.5.3-SNAPSHOT' repositories { maven { url '/tmp/repo/' } @@ -37,11 +37,16 @@ dependencies { //packer-begin packer { - archiveNameFormat = '${appPkg}-${buildType}-v${versionName}-${channel}' - archiveOutput = new File(project.rootProject.buildDir, "myapks") - channelFile = new File(project.rootDir, "markets.txt") - channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', - 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] + archiveNameFormat = '${projectName}-${buildType}-v${versionName}-${channel}' + archiveOutput = new File(project.rootProject.buildDir, "apks") +// channelFile = new File(project.rootDir, "markets.txt") +// channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', +// 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] + channelMap = [ + "Cat" : project.rootProject.file("channels/cat.txt"), + "Dog" : project.rootProject.file("channels/dog.txt"), + "Fish": project.rootProject.file("channels/markets.txt") + ] } //packer-end From ad7c42bf1fc7fcbbd094aaa8cd8f4ee4586eb23e Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Mon, 5 Jun 2017 18:26:03 +0800 Subject: [PATCH 20/67] rename some methods, tweak help --- .../main/resources/com/mcxiaoke/packer/cli/help.txt | 4 ++-- gradle.properties | 2 +- .../com/mcxiaoke/packer/ng/GradlePlugin.groovy | 4 ++-- .../groovy/com/mcxiaoke/packer/ng/GradleTask.groovy | 2 +- sample/build.gradle | 12 ++++++------ 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt b/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt index 16f2e04..caea396 100644 --- a/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt +++ b/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt @@ -19,8 +19,8 @@ EXAMPLE generate Add channel info to the provided APK - packer-ng pack --channels=ch1,ch2,ch3 --output=archives app.apk - packer-ng pack --channels=@file.txt --output=archives app.apk + packer-ng generate --channels=ch1,ch2,ch3 --output=archives app.apk + packer-ng generate --channels=@file.txt --output=archives app.apk --channels=@file.txt - using channels from the provided file. --channels=ch1,ch2,ch3 - using channels from the provided list. diff --git a/gradle.properties b/gradle.properties index d0ef227..026828c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.5.3-SNAPSHOT +VERSION_NAME=1.5.4-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy index 0c687f7..b9cd879 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy @@ -49,7 +49,7 @@ class GradlePlugin implements Plugin { void addTasks(BaseVariant vt) { debug("addTasks() for ${vt.name}") - def variantTask = project.task("generate${vt.name.capitalize()}Channels", + def variantTask = project.task("apk${vt.name.capitalize()}", type: GradleTask) { variant = vt extension = project.packer @@ -60,7 +60,7 @@ class GradlePlugin implements Plugin { def buildTypeName = vt.buildType.name if (vt.name != buildTypeName) { - def taskName = "generate${buildTypeName.capitalize()}Channels" + def taskName = "apk${buildTypeName.capitalize()}" def task = project.tasks.findByName(taskName) if (task == null) { task = project.task(taskName) diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy index f348787..7ed5401 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy @@ -134,7 +134,7 @@ class GradleTask extends DefaultTask { channels = readChannels(f) } if (channels == null || channels.isEmpty()) { - throw new PluginException("channels is null or empty") + throw new PluginException("No channels found") } return escape(channels) } diff --git a/sample/build.gradle b/sample/build.gradle index f776579..2363fab 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.5.3-SNAPSHOT' + ext.packer_version = '1.5.4-SNAPSHOT' repositories { maven { url '/tmp/repo/' } @@ -42,11 +42,11 @@ packer { // channelFile = new File(project.rootDir, "markets.txt") // channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', // 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] - channelMap = [ - "Cat" : project.rootProject.file("channels/cat.txt"), - "Dog" : project.rootProject.file("channels/dog.txt"), - "Fish": project.rootProject.file("channels/markets.txt") - ] +// channelMap = [ +// "Cat" : project.rootProject.file("channels/cat.txt"), +// "Dog" : project.rootProject.file("channels/dog.txt"), +// "Fish": project.rootProject.file("channels/markets.txt") +// ] } //packer-end From 0eb42383a9f3fea5175ca671d930b0fcf1872442 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Tue, 6 Jun 2017 13:34:15 +0800 Subject: [PATCH 21/67] tweak apk generating, minor fix --- .gitignore | 2 ++ .../java/com/mcxiaoke/packer/cli/Main.java | 8 ++--- .../com/mcxiaoke/packer/cli/help.txt | 2 -- gradle.properties | 2 +- .../com/mcxiaoke/packer/ng/GradleTask.groovy | 35 +++++++++++++------ sample/build.gradle | 8 ++--- 6 files changed, 36 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index cc165d3..83afda7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ build/ apks/ repo/ dist/ +tmp/ +channels/ *.iml *.apk *.pyc diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java index 99ae0ca..a2fbc14 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java @@ -38,9 +38,9 @@ public static void main(String[] args) { } else if ("version".equals(cmd)) { printUsage(); } else { - printUsage(); System.err.println( "Unsupported command: " + cmd); + printUsage(); } } catch (Exception e) { System.err.println("Error: " + e.getMessage()); @@ -166,9 +166,9 @@ private static void doVerify(File apkFile) throws IOException { } final boolean verified = Bridge.verifyApk(apkFile); final String channel = Bridge.readChannel(apkFile); - System.out.println("File: " + apkFile); - System.out.println("Signed:" + verified); - System.out.println("Channel:" + channel); + System.out.println("File: " + apkFile.getName()); + System.out.println("Signed: " + verified); + System.out.println("Channel: " + channel); } diff --git a/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt b/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt index caea396..a661e86 100644 --- a/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt +++ b/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt @@ -30,5 +30,3 @@ generate Add channel info to the provided APK verify Check whether signatures and channel of the provided APK is valid. packer-ng verify app.apk - - diff --git a/gradle.properties b/gradle.properties index 026828c..701ade9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.5.4-SNAPSHOT +VERSION_NAME=1.6.6-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy index 7ed5401..e015d44 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy @@ -31,7 +31,7 @@ class GradleTask extends DefaultTask { Template getNameTemplate() { String format - String propValue = project.findProperty(Const.PROP_OUTPUT) + String propValue = project.findProperty(Const.PROP_FORMAT) if (propValue != null) { format = propValue.toString() } else { @@ -44,7 +44,10 @@ class GradleTask extends DefaultTask { return engine.createTemplate(format) } - File getOriginalApkWithCheck() { + File getOriginalApk() { + variant.outputs.each { ot -> + logger.info("Output APK: ${ot.name},${ot.outputFile}") + } File file = variant.outputs[0].outputFile if (!Bridge.verifyApk(file)) { throw new PluginException("APK Signature Scheme v2 verify failed: '${file}'") @@ -52,7 +55,7 @@ class GradleTask extends DefaultTask { return file } - File getOutputWithCheck() { + File getOutputRoot() { File outputDir String propValue = project.findProperty(Const.PROP_OUTPUT) if (propValue != null) { @@ -64,6 +67,14 @@ class GradleTask extends DefaultTask { if (outputDir == null) { outputDir = new File(project.buildDir, Const.DEFAULT_OUTPUT) } + if (!outputDir.exists()) { + outputDir.mkdirs() + } + return outputDir + } + + File getVariantOutput() { + File outputDir = getOutputRoot() String flavorName = variant.flavorName if (flavorName.length() > 0) { outputDir = new File(outputDir, flavorName) @@ -82,7 +93,7 @@ class GradleTask extends DefaultTask { return outputDir } - Set getChannelsWithCheck() { + Set getChannels() { // -P channels=ch1,ch2,ch3 // -P channels=@channels.txt // channelList = [ch1,ch2,ch3] @@ -156,16 +167,17 @@ class GradleTask extends DefaultTask { println("PackerNg - https://github.com/mcxiaoke/packer-ng-plugin") println("============================================================") showProperties() - File apkFile = getOriginalApkWithCheck() - File outputDir = getOutputWithCheck() - Collection channels = getChannelsWithCheck() + File apkFile = getOriginalApk() + File rootDir = getOutputRoot() + File outputDir = getVariantOutput() + Collection channels = getChannels() Template template = getNameTemplate() println("Variant: ${variant.name}") println("Input: ${apkFile.path}") println("Output: ${outputDir.path}") - println("Channels: [${channels.join(', ')}]") + println("Channels: [${channels.join('/')}]") for (String channel : channels) { - File tempFile = new File(outputDir, channel + ".tmp") + File tempFile = new File(outputDir, "tmp-${channel}.apk") copyTo(apkFile, tempFile) try { Bridge.writeChannel(tempFile, channel) @@ -174,6 +186,7 @@ class GradleTask extends DefaultTask { if (Bridge.verifyChannel(tempFile, channel)) { println("--> Generating: ${apkName}") tempFile.renameTo(finalFile) + logger.info("Generated: ${finalFile}") } else { throw new PluginException("${channel} APK verify failed") } @@ -183,7 +196,7 @@ class GradleTask extends DefaultTask { tempFile.delete() } } - println("Outputs: ${outputDir.absolutePath}") + println("Outputs: ${rootDir.absolutePath}") println("============================================================") } @@ -202,10 +215,12 @@ class GradleTask extends DefaultTask { 'appPkg' : variant.applicationId, 'buildTime' : buildTime ] + logger.info("nameMap: ${nameMap}") return template.make(nameMap).toString() + '.apk' } static Set escape(Collection cs) { + // filter invalid chars for filename Pattern pattern = ~/[\/:*?"'<>|]/ return cs.collect { it.replaceAll(pattern, "_") }.toSet() } diff --git a/sample/build.gradle b/sample/build.gradle index 2363fab..0727c5a 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.5.4-SNAPSHOT' + ext.packer_version = '1.6.6-SNAPSHOT' repositories { maven { url '/tmp/repo/' } @@ -39,13 +39,13 @@ dependencies { packer { archiveNameFormat = '${projectName}-${buildType}-v${versionName}-${channel}' archiveOutput = new File(project.rootProject.buildDir, "apks") + channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', + 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] // channelFile = new File(project.rootDir, "markets.txt") -// channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', -// 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] // channelMap = [ // "Cat" : project.rootProject.file("channels/cat.txt"), // "Dog" : project.rootProject.file("channels/dog.txt"), -// "Fish": project.rootProject.file("channels/markets.txt") +// "Fish": project.rootProject.file("channels/channels.txt") // ] } //packer-end From 225a3cea04bc228b51b5204d26ffa96d46fee7d2 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Tue, 6 Jun 2017 13:46:01 +0800 Subject: [PATCH 22/67] release v1.7.0 for test --- cli/build.gradle | 6 +++--- gradle.properties | 2 +- sample/build.gradle | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/build.gradle b/cli/build.gradle index 48b1b82..b3b35f1 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -11,7 +11,7 @@ repositories { } apply plugin: 'java' -apply plugin: 'application' +//apply plugin: 'application' sourceCompatibility = 1.7 targetCompatibility = 1.7 @@ -21,7 +21,7 @@ dependencies { compile 'com.android.tools.build:apksig:2.3.2' } -mainClassName = 'com.mcxiaoke.packer.cli.Main' +//mainClassName = 'com.mcxiaoke.packer.cli.Main' task fatJar(type: Jar) { with jar @@ -42,7 +42,7 @@ task fatJar(type: Jar) { task distJar(type: Copy, dependsOn: fatJar) { from fatJar.outputs.files - into project.rootProject.file('dist') + into project.rootProject.file('tools') } // apply from: '../jar.gradle' diff --git a/gradle.properties b/gradle.properties index 701ade9..d97eb05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.6.6-SNAPSHOT +VERSION_NAME=1.7.0 VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/sample/build.gradle b/sample/build.gradle index 0727c5a..b8b2701 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.6.6-SNAPSHOT' + ext.packer_version = '1.7.0-SNAPSHOT' repositories { maven { url '/tmp/repo/' } From 60e483b29368ea102062638029bcb14835e1a199 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 7 Jun 2017 11:21:34 +0800 Subject: [PATCH 23/67] change channel key and block id --- .../java/com/mcxiaoke/packer/cli/Bridge.java | 6 +- .../com/mcxiaoke/packer/common/CPacker.java | 47 ++++++++++++++ .../common/{Payload.java => CPayload.java} | 56 +++++++++++------ .../com/mcxiaoke/packer/common/Parser.java | 61 ------------------- .../mcxiaoke/packer/common/PayloadTests.java | 34 +++++------ gradle.properties | 2 +- .../com/mcxiaoke/packer/helper/PackerNg.java | 7 +-- sample/build.gradle | 16 ++--- 8 files changed, 117 insertions(+), 112 deletions(-) create mode 100644 common/src/main/java/com/mcxiaoke/packer/common/CPacker.java rename common/src/main/java/com/mcxiaoke/packer/common/{Payload.java => CPayload.java} (59%) delete mode 100644 common/src/main/java/com/mcxiaoke/packer/common/Parser.java diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java index e72e9d5..e73b3bc 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java @@ -4,7 +4,7 @@ import com.android.apksig.ApkVerifier.Builder; import com.android.apksig.ApkVerifier.Result; import com.android.apksig.apk.ApkFormatException; -import com.mcxiaoke.packer.common.Parser; +import com.mcxiaoke.packer.common.CPacker; import java.io.File; import java.io.IOException; @@ -18,11 +18,11 @@ public class Bridge { public static void writeChannel(File file, String channel) throws IOException { - Parser.create(file).writeChannel(channel); + CPacker.of(file).writeChannel(channel); } public static String readChannel(File file) throws IOException { - return Parser.create(file).readChannel(); + return CPacker.of(file).readChannel(); } public static boolean verifyChannel(File file, String channel) throws IOException { diff --git a/common/src/main/java/com/mcxiaoke/packer/common/CPacker.java b/common/src/main/java/com/mcxiaoke/packer/common/CPacker.java new file mode 100644 index 0000000..7b12360 --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/common/CPacker.java @@ -0,0 +1,47 @@ +package com.mcxiaoke.packer.common; + +import java.io.File; +import java.io.IOException; + +/** + * User: mcxiaoke + * Date: 2017/5/17 + * Time: 15:39 + */ +public class CPacker { + + + public static CPacker of(File apkFile) { + return new CPacker(apkFile, PLUGIN_CHANNEL_KEY, PLUGIN_BLOCK_ID); + } + + // channel info key + public static final String PLUGIN_CHANNEL_KEY = "zKey"; // 0x7a4b6579 + // channel extra key + public static final String PLUGIN_EXTRA_KEY = "zExt"; // 0x7a457874 + // plugin block id + public static final int PLUGIN_BLOCK_ID = 0x7a786b21; // "zxk!" + + + private File apkFile; + private String key; + private int blockId; + + CPacker(final File apkFile, + final String key, + final int blockId) { + this.apkFile = apkFile; + this.key = key; + this.blockId = blockId; + } + + public String readChannel() throws IOException { + return CPayload.readValue(apkFile, key, blockId); + } + + public void writeChannel(final String channel) throws IOException { + CPayload.writeValue(apkFile, key, channel, blockId); + } + + +} diff --git a/common/src/main/java/com/mcxiaoke/packer/common/Payload.java b/common/src/main/java/com/mcxiaoke/packer/common/CPayload.java similarity index 59% rename from common/src/main/java/com/mcxiaoke/packer/common/Payload.java rename to common/src/main/java/com/mcxiaoke/packer/common/CPayload.java index 583f34c..efe5135 100644 --- a/common/src/main/java/com/mcxiaoke/packer/common/Payload.java +++ b/common/src/main/java/com/mcxiaoke/packer/common/CPayload.java @@ -5,7 +5,11 @@ import java.io.File; import java.io.IOException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; @@ -14,43 +18,47 @@ * Date: 2017/5/26 * Time: 13:18 */ -public class Payload { +public class CPayload { // charset utf8 - public static final String UTF8 = "UTF-8"; + private static final String UTF8 = "UTF-8"; - public static String readChannel(File apkFile, - String channelKey, - int blockId) throws IOException { + public static String readValue(File apkFile, + String key, + int blockId) throws IOException { final Map map = readValues(apkFile, blockId); if (map == null || map.isEmpty()) { return null; } - return map.get(channelKey); + return map.get(key); } - public static void writeChannel(File apkFile, - String channel, - String channelKey, - int blockId) throws IOException { + public static void writeValue(File apkFile, + String key, + String value, + int blockId) throws IOException { final Map values = new HashMap<>(); - values.put(channelKey, channel); + values.put(key, value); writeValues(apkFile, values, blockId); } public static Map readValues(File apkFile, int blockId) throws IOException { - final String content = readRaw(apkFile, blockId); + final String content = readString(apkFile, blockId); return mapFromString(content); } - public static String readRaw(File apkFile, int blockId) throws IOException { - final byte[] bytes = PayloadReader.readBlock(apkFile, blockId); + public static String readString(File apkFile, int blockId) throws IOException { + final byte[] bytes = readBytes(apkFile, blockId); if (bytes == null || bytes.length == 0) { return null; } return new String(bytes, UTF8); } + public static byte[] readBytes(File apkFile, int blockId) throws IOException { + return PayloadReader.readBlock(apkFile, blockId); + } + public static void writeValues(File apkFile, Map values, int blockId) throws IOException { if (values == null || values.isEmpty()) { @@ -62,16 +70,21 @@ public static void writeValues(File apkFile, Map values, int blo newValues.putAll(oldValues); } newValues.putAll(values); - writeRaw(apkFile, mapToString(newValues), blockId); + writeString(apkFile, mapToString(newValues), blockId); } - public static void writeRaw(File apkFile, final String content, int blockId) + public static void writeString(File apkFile, final String content, int blockId) throws IOException { PayloadWriter.writeBlock(apkFile, blockId, content.getBytes(UTF8)); } - private static final String SEP_KV = "\u2218"; - private static final String SEP_LINE = "\u2219"; + public static void writeBytes(File apkFile, final byte[] bytes, int blockId) + throws IOException { + PayloadWriter.writeBlock(apkFile, blockId, bytes); + } + + public static final String SEP_KV = "∘";//\u2218 + public static final String SEP_LINE = "∙";//\u2219 private static String mapToString(final Map map) throws IOException { if (map == null || map.isEmpty()) { @@ -100,4 +113,11 @@ private static Map mapFromString(final String string) { } return map; } + + private static final String DATE_FORMAT = "yyyy/MM/dd HH:mm:ss Z"; + + private static String getDateString() { + final DateFormat df = new SimpleDateFormat(DATE_FORMAT, Locale.US); + return df.format(new Date()); + } } diff --git a/common/src/main/java/com/mcxiaoke/packer/common/Parser.java b/common/src/main/java/com/mcxiaoke/packer/common/Parser.java deleted file mode 100644 index 48af1c7..0000000 --- a/common/src/main/java/com/mcxiaoke/packer/common/Parser.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.mcxiaoke.packer.common; - -import java.io.File; -import java.io.IOException; - -/** - * User: mcxiaoke - * Date: 2017/5/17 - * Time: 15:39 - */ -public class Parser { - - - public static Parser create(File apkFile) { - return new Parser(apkFile); - } - - public static Parser create(File apkFile, String channelKey) { - return new Parser(apkFile, channelKey); - } - - public static Parser create(File apkFile, String channelKey, int channelBlockId) { - return new Parser(apkFile, channelKey, channelBlockId); - } - - // channel info key - public static final String DEFAULT_CHANNEL_KEY = "0x4d6975"; - // channel info id - public static final int DEFAULT_CHANNEL_BLOCK_ID = 0x717a786b; - - - private File apkFile; - private String channelKey; - private int channelBlockId; - - Parser(final File apkFile) { - this(apkFile, DEFAULT_CHANNEL_KEY, DEFAULT_CHANNEL_BLOCK_ID); - } - - Parser(final File apkFile, final String channelKey) { - this(apkFile, channelKey, DEFAULT_CHANNEL_BLOCK_ID); - } - - Parser(final File apkFile, - final String channelKey, - final int channelBlockId) { - this.apkFile = apkFile; - this.channelKey = channelKey; - this.channelBlockId = channelBlockId; - } - - public String readChannel() throws IOException { - return Payload.readChannel(apkFile, channelKey, channelBlockId); - } - - public void writeChannel(final String channel) throws IOException { - Payload.writeChannel(apkFile, channel, channelKey, channelBlockId); - } - - -} diff --git a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java index 381a378..d989d6d 100644 --- a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java +++ b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java @@ -77,8 +77,8 @@ public void testOverrideSignature() throws IOException, NoSuchAlgorithmException { File f = newTestFile(); // don't write with APK Signature Scheme v2 Block ID 0x7109871a - Payload.writeRaw(f, "OverrideSignatureSchemeBlock", 0x7109871a); - assertEquals("OverrideSignatureSchemeBlock", Payload.readRaw(f, 0x7109871a)); + CPayload.writeString(f, "OverrideSignatureSchemeBlock", 0x7109871a); + assertEquals("OverrideSignatureSchemeBlock", CPayload.readString(f, 0x7109871a)); ApkVerifier verifier = new Builder(f).build(); Result result = verifier.verify(); final List errors = result.getErrors(); @@ -113,10 +113,10 @@ public void testBytesWrite2() throws IOException { public void testStringWrite() throws IOException { File f = newTestFile(); - Payload.writeRaw(f, "Test String", 0x717a786b); - assertEquals("Test String", Payload.readRaw(f, 0x717a786b)); - Payload.writeRaw(f, "中文和特殊符号测试!@#¥%……*()《》?:【】、", 0x717a786b); - assertEquals("中文和特殊符号测试!@#¥%……*()《》?:【】、", Payload.readRaw(f, 0x717a786b)); + CPayload.writeString(f, "Test String", 0x717a786b); + assertEquals("Test String", CPayload.readString(f, 0x717a786b)); + CPayload.writeString(f, "中文和特殊符号测试!@#¥%……*()《》?:【】、", 0x717a786b); + assertEquals("中文和特殊符号测试!@#¥%……*()《》?:【】、", CPayload.readString(f, 0x717a786b)); checkApkVerified(f); } @@ -127,8 +127,8 @@ public void testValuesWrite() throws IOException { in.put("名字", "哈哈啊哈哈哈"); in.put("!@#$!%^@&*()_+\"?:><", "渠道Google"); in.put("12345abcd", "2017"); - Payload.writeValues(f, in, 0x12345); - Map out = Payload.readValues(f, 0x12345); + CPayload.writeValues(f, in, 0x12345); + Map out = CPayload.readValues(f, 0x12345); assertNotNull(out); assertEquals(in.size(), out.size()); for (Map.Entry entry : in.entrySet()) { @@ -142,19 +142,19 @@ public void testValuesMixedWrite() throws IOException { Map in = new HashMap<>(); in.put("!@#$!%^@&*()_+\"?:><", "渠道Google"); in.put("12345abcd", "2017"); - Payload.writeValues(f, in, 0x123456); - Payload.writeChannel(f, "Mixed", "hello", 0x8888); - Map out = Payload.readValues(f, 0x123456); + CPayload.writeValues(f, in, 0x123456); + CPayload.writeValue(f, "Mixed", "hello", 0x8888); + Map out = CPayload.readValues(f, 0x123456); assertNotNull(out); assertEquals(in.size(), out.size()); for (Map.Entry entry : in.entrySet()) { assertEquals(entry.getValue(), out.get(entry.getKey())); } - assertEquals("Mixed", Payload.readChannel(f, "hello", 0x8888)); - Payload.writeRaw(f, "RawValue", 0x2017); - assertEquals("RawValue", Payload.readRaw(f, 0x2017)); - Payload.writeRaw(f, "OverrideValues", 0x123456); - assertEquals("OverrideValues", Payload.readRaw(f, 0x123456)); + assertEquals("Mixed", CPayload.readValue(f, "hello", 0x8888)); + CPayload.writeString(f, "RawValue", 0x2017); + assertEquals("RawValue", CPayload.readString(f, 0x2017)); + CPayload.writeString(f, "OverrideValues", 0x123456); + assertEquals("OverrideValues", CPayload.readString(f, 0x123456)); checkApkVerified(f); } @@ -218,7 +218,7 @@ public void testBufferWrite() throws IOException { public void testChannelWriteRead() throws IOException { File f = newTestFile(); - Parser p = new Parser(f); + CPacker p = CPacker.of(f); p.writeChannel("Hello"); assertEquals("Hello", p.readChannel()); p.writeChannel("中文"); diff --git a/gradle.properties b/gradle.properties index d97eb05..5949ac1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.7.0 +VERSION_NAME=1.7.1-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java index 82e806d..79e9b17 100644 --- a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java +++ b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java @@ -2,7 +2,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; -import com.mcxiaoke.packer.common.Parser; +import com.mcxiaoke.packer.common.CPacker; import java.io.File; @@ -12,7 +12,7 @@ * Time: 13:12 */ public final class PackerNg { - private static final String TAG = PackerNg.class.getSimpleName(); + private static final String TAG = "PackerNg"; private static final String EMPTY_STRING = ""; private static String sCachedChannel; @@ -44,8 +44,7 @@ private static ChannelInfo getMarketInternal(final Context context, try { final ApplicationInfo info = context.getApplicationInfo(); final File apkFile = new File(info.sourceDir); - final Parser parser = Parser.create(apkFile); - market = parser.readChannel(); + market = CPacker.of(apkFile).readChannel(); } catch (Exception e) { error = e; } diff --git a/sample/build.gradle b/sample/build.gradle index b8b2701..a869313 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.7.0-SNAPSHOT' + ext.packer_version = '1.7.1-SNAPSHOT' repositories { maven { url '/tmp/repo/' } @@ -39,14 +39,14 @@ dependencies { packer { archiveNameFormat = '${projectName}-${buildType}-v${versionName}-${channel}' archiveOutput = new File(project.rootProject.buildDir, "apks") - channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', - 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] +// channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', +// 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] // channelFile = new File(project.rootDir, "markets.txt") -// channelMap = [ -// "Cat" : project.rootProject.file("channels/cat.txt"), -// "Dog" : project.rootProject.file("channels/dog.txt"), -// "Fish": project.rootProject.file("channels/channels.txt") -// ] + channelMap = [ + "Cat" : project.rootProject.file("channels/cat.txt"), + "Dog" : project.rootProject.file("channels/dog.txt"), + "Fish": project.rootProject.file("channels/channels.txt") + ] } //packer-end From 5492cbaafc2b054dc401c088f7ef26b65a3b79a0 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 7 Jun 2017 11:36:40 +0800 Subject: [PATCH 24/67] add first version python script --- tools/PackerNg-1.0.9.jar | Bin 10543 -> 0 bytes tools/packer-ng-v2.py | 262 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) delete mode 100644 tools/PackerNg-1.0.9.jar create mode 100644 tools/packer-ng-v2.py diff --git a/tools/PackerNg-1.0.9.jar b/tools/PackerNg-1.0.9.jar deleted file mode 100644 index d06a18b830935fc660f1022f431688c097da1c0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10543 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 APK_SIG_BLOCK_MIN_SIZE: + fStart = centralDirStartOffset-24 + fEnd = centralDirStartOffset + footer = mm[fStart:fEnd] + footerSize = len(footer) + # print('footer:',to_hex(footer)) + mlo = struct.unpack('= footerSize and apkSigBlockSizeInFooter < sys.maxint - 8: + totalSize = apkSigBlockSizeInFooter + 8 + print('totalSize:', totalSize) + apkSigBlockOffset = centralDirStartOffset - totalSize + print('apkSigBlockOffset:', apkSigBlockOffset) + if apkSigBlockOffset >= 0: + apkSigBlock = mm[apkSigBlockOffset:apkSigBlockOffset+8] + # print('apkSigBlock:', to_hex(apkSigBlock)) + asbh = struct.unpack(' sys.maxint - 8: + print('APK Signing Block entry size out of range 1') + break + nextEntryPos = position + lenLong + print('nextEntryPos', nextEntryPos) + if nextEntryPos > pairsSize: + print('APK Signing Block entry size out of range 2') + break + sid = struct.unpack(' Date: Wed, 7 Jun 2017 15:01:45 +0800 Subject: [PATCH 25/67] python script optimize, refactory to functions --- tools/packer-ng-v2.py | 479 ++++++++++++++++++++++++++---------------- 1 file changed, 301 insertions(+), 178 deletions(-) diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index d4c5246..8e63b44 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -2,14 +2,22 @@ # @Author: mcxiaoke # @Date: 2017-06-06 14:03:18 # @Last Modified by: mcxiaoke -# @Last Modified time: 2017-06-07 11:33:29 +# @Last Modified time: 2017-06-07 14:58:50 from __future__ import print_function +# from __future__ import unicode_literals import os import sys import mmap import struct +import zipfile +import logging + +logging.basicConfig(format='%(levelname)s:%(lineno)s:%(funcName)s() %(message)s', level=logging.ERROR) +logger = logging.getLogger(__name__) # ref: https://android.googlesource.com/platform/tools/apksig/+/master +# ref: https://source.android.com/security/apksigning/v2 + ZIP_EOCD_REC_MIN_SIZE = 22 ZIP_EOCD_REC_SIG = 0x06054b50 ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10 @@ -45,17 +53,67 @@ except Exception as e: VERSION = '1.0.0' -print('AUTHOR:', AUTHOR) -print('VERSION:', VERSION) +logger.debug('AUTHOR:%s', AUTHOR) +logger.debug('VERSION:%s', VERSION) APK1 = 'apks/Cat/packer-ng-release-v1.7.1-SNAPSHOT-田园猫.apk' APK2 = 'apks/Cat/packer-ng-release-v1.7.1-SNAPSHOT-Special@Cat%001.apk' APK3 = 'apks/Fish/packer-ng-release-v1.7.1-SNAPSHOT-2017年.apk' -APK4 = 'apks/sample-Cat-release.apk' +APK_RELEASE = 'apks/sample-Cat-release.apk' +APK_BETA = 'apks/sample-Cat-beta.apk' +APK_DEBUG = 'apks/sample-Cat-debug.apk' +ZIP_NOT_APK = 'cli.zip' +TXT_NOT_APK = 'cv.java' APK = APK1 +class ZipFormatException(Exception): + pass + + +class SignatureNotFoundException(Exception): + pass + + +class ByteDecoder(object): + ''' + byte array decoder + https://docs.python.org/2/library/struct.html + ''' + + def __init__(self, buf, littleEndian=True): + self.buf = buf + self.sign = '<' if littleEndian else '>' + + def getShort(self, offset=0): + return struct.unpack('{}h'.format(self.sign), self.buf[offset:offset+2])[0] + + def getUShort(self, offset=0): + return struct.unpack('{}H'.format(self.sign), self.buf[offset:offset+2])[0] + + def getInt(self, offset=0): + return struct.unpack('{}i'.format(self.sign), self.buf[offset:offset+4])[0] + + def getUInt(self, offset=0): + return struct.unpack('{}I'.format(self.sign), self.buf[offset:offset+4])[0] + + def getLong(self, offset=0): + return struct.unpack('{}q'.format(self.sign), self.buf[offset:offset+8])[0] + + def getULong(self, offset=0): + return struct.unpack('{}Q'.format(self.sign), self.buf[offset:offset+8])[0] + + def getFloat(self, offset=0): + return struct.unpack('{}f'.format(self.sign), self.buf[offset:offset+4])[0] + + def getDouble(self, offset=0): + return struct.unpack('{}d'.format(self.sign), self.buf[offset:offset+8])[0] + + def getChars(self, offset=0, size=16): + return struct.unpack('{}{}'.format(self.sign, 's'*size), self.buf[offset:offset+size]) + + class ZipSections(object): ''' long centralDirectoryOffset, @@ -78,185 +136,250 @@ def __init__(self, cdStartOffset, self.eocd = eocd -def to_hex(s): - return " ".join("{:02x}".format(ord(c)) for c in s) +def getChannel(apk): + apk = os.path.abspath(apk) + logger.debug('apk:%s', apk) + values = findPluginBlockValues(apk) + if values: + channel = values.get(PLUGIN_CHANNEL_KEY) + extra = values.get(PLUGIN_EXTRA_KEY) + logger.debug('channel:%s', channel) + logger.debug('extra:%s', extra) + return channel + else: + logger.debug('channel not found') -def show_info(apk): +def findPluginBlockValues(apk): + apkSigningBlock = findApkSigningBlock(apk) + block = parseApkSigningBlock(apkSigningBlock, PLUGIN_BLOCK_ID) + if block: + values = dict(line.split(SEP_KV) for line in block.split(SEP_LINE) if line.strip()) + logger.debug('values:%s', values) + return values + + +def findApkSigningBlock(apk): + zp = zipfile.ZipFile(apk) + zp.testzip() with open(apk, "r+b") as f: mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) - fileSize = mm.size() - print('fileSize:', fileSize) - # 99.99% of APKs have a zero-length comment field - maxCommentSize = min(UINT16_MAX_VALUE, fileSize - ZIP_EOCD_REC_MIN_SIZE) - maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize - print('maxCommentSize:', maxCommentSize) - print('maxEocdSize:', maxEocdSize) - bufOffsetInFile = fileSize - maxEocdSize - print('bufOffsetInFile:', bufOffsetInFile) - # buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize) - buf = mm[bufOffsetInFile:bufOffsetInFile+maxEocdSize] - # print('buf:',to_hex(buf)) - archiveSize = len(buf) - print('archiveSize:', archiveSize) - maxCommentLength = min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE) - print('maxCommentLength:', maxCommentLength) - eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE - print('eocdWithEmptyCommentStartPosition:', eocdWithEmptyCommentStartPosition) - - ''' - for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength; - expectedCommentLength++) { - int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; - if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { - int actualCommentLength = - getUnsignedInt16( - zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); - if (actualCommentLength == expectedCommentLength) { - return eocdStartPos; - } - } - } - ''' - expectedCommentLength = 0 - eocdOffsetInBuf = -1 - while expectedCommentLength <= maxCommentLength: - eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength - print('expectedCommentLength:', expectedCommentLength) - print('eocdStartPos:', eocdStartPos) - print('unpack:', to_hex(buf[eocdStartPos:eocdStartPos+4])) - seg = struct.unpack('= APK_SIG_BLOCK_MIN_SIZE: - fStart = centralDirStartOffset-24 - fEnd = centralDirStartOffset - footer = mm[fStart:fEnd] - footerSize = len(footer) - # print('footer:',to_hex(footer)) - mlo = struct.unpack('= footerSize and apkSigBlockSizeInFooter < sys.maxint - 8: - totalSize = apkSigBlockSizeInFooter + 8 - print('totalSize:', totalSize) - apkSigBlockOffset = centralDirStartOffset - totalSize - print('apkSigBlockOffset:', apkSigBlockOffset) - if apkSigBlockOffset >= 0: - apkSigBlock = mm[apkSigBlockOffset:apkSigBlockOffset+8] - # print('apkSigBlock:', to_hex(apkSigBlock)) - asbh = struct.unpack(' sys.maxint - 8: - print('APK Signing Block entry size out of range 1') - break - nextEntryPos = position + lenLong - print('nextEntryPos', nextEntryPos) - if nextEntryPos > pairsSize: - print('APK Signing Block entry size out of range 2') - break - sid = struct.unpack(' sys.maxint - 8: + raise SignatureNotFoundException( + "APK Signing Block size out of range: " + apkSigBlockSizeInFooter) + + totalSize = apkSigBlockSizeInFooter + 8 + logger.debug('totalSize:%s', totalSize) + apkSigBlockOffset = centralDirStartOffset - totalSize + logger.debug('apkSigBlockOffset:%s', apkSigBlockOffset) + + if apkSigBlockOffset < 0: + raise SignatureNotFoundException( + "APK Signing Block offset out of range: " + apkSigBlockOffset) + + apkSigBlock = mm[apkSigBlockOffset:apkSigBlockOffset+8] + # logger.debug('apkSigBlock:%s', to_hex(apkSigBlock)) + apkSigBlockSizeInHeader = ByteDecoder(apkSigBlock).getLong(0) + logger.debug('apkSigBlockSizeInHeader:%s', apkSigBlockSizeInHeader) + + if apkSigBlockSizeInHeader != apkSigBlockSizeInFooter: + raise SignatureNotFoundException( + "APK Signing Block sizes in header and footer do not match: " + + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter) + + apkSigningBlock = mm[apkSigBlockOffset:apkSigBlockOffset+totalSize] + return apkSigningBlock + + +def parseApkSigningBlock(block, blockId): + ''' + // APK Signing Block + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes(excluding this field) + // * @+8 bytes payload + // * @-24 bytes uint64: size in bytes(same as the one above) + // * @-16 bytes uint128: magic + ''' + block = block[8:-24] # only payload + bd = ByteDecoder(block) + size = len(block) + logger.debug('size:%s', size) + + entryCount = 0 + position = 0 + signingBlock = None + channelBlock = None + while position < size: + entryCount += 1 + logger.debug('----------') + logger.debug('entryCount:%s', entryCount) + if size - position < 8: + raise SignatureNotFoundException('Insufficient data to read size of APK Signing Block entry: {}'.format(entryCount)) + lenLong = bd.getLong(position) + logger.debug('lenLong:%s', lenLong) + position += 8 + if lenLong < 4 or lenLong > sys.maxint - 8: + raise SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + + " size out of range: " + lenLong) + nextEntryPos = position + lenLong + logger.debug('nextEntryPos:%s', nextEntryPos) + if nextEntryPos > size: + SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + " size out of range: " + len + + ", available: " + (size - position)) + sid = bd.getInt(position) + logger.debug('blockId:%s', hex(sid)) + position += 4 + if sid == APK_SIGNATURE_SCHEME_V2_BLOCK_ID: + logger.debug('found signingBlock') + signingBlock = block[position:position+lenLong-4] + signingBlockSize = len(signingBlock) + logger.debug('signingBlockSize:%s', signingBlockSize) + # logger.debug('signingBlockHex:%s', to_hex(signingBlock[0:32])) + elif sid == blockId: + logger.debug('found pluginBlock') + pluginBlock = block[position:position+lenLong-4] + pluginBlockSize = len(pluginBlock) + logger.debug('pluginBlockSize:%s', pluginBlockSize) + logger.debug('pluginBlock:%s', pluginBlock) + # logger.debug('pluginBlockHex:%s', to_hex(pluginBlock)) + return pluginBlock else: - eocdOffset = -1 - eocdBuf = None - print('eocd start offset not found.') + logger.debug('found unknown block:%s', hex(sid)) + position = nextEntryPos + + +def findZipSections(mm): + eocd = findEocdRecord(mm) + if not eocd: + raise ZipFormatException("ZIP End of Central Directory record not found") + eocdOffset, eocdBuf = eocd + ed = ByteDecoder(eocdBuf) + # logger.debug('eocdBuf:%s', to_hex(eocdBuf)) + cdStartOffset = ed.getUInt(ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET) + logger.debug('cdStartOffset:%s', cdStartOffset) + if cdStartOffset > eocdOffset: + raise ZipFormatException( + "ZIP Central Directory start offset out of range: " + cdStartOffset + + ". ZIP End of Central Directory offset: " + eocdOffset) + cdSizeBytes = ed.getUInt(ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET) + logger.debug('cdSizeBytes:%s', cdSizeBytes) + cdEndOffset = cdStartOffset + cdSizeBytes + logger.debug('cdEndOffset:%s', cdEndOffset) + if cdEndOffset > eocdOffset: + raise ZipFormatException( + "ZIP Central Directory overlaps with End of Central Directory" + + ". CD end: " + cdEndOffset + + ", EoCD start: " + eocdOffset) + cdRecordCount = ed.getUShort(ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET) + logger.debug('cdRecordCount:%s', cdRecordCount) + sections = ZipSections(cdStartOffset, + cdSizeBytes, + cdRecordCount, + eocdOffset, + eocdBuf) + return sections + + +def findEocdRecord(mm): + fileSize = mm.size() + logger.debug('fileSize:%s', fileSize) + if fileSize < ZIP_EOCD_REC_MIN_SIZE: + return None + + # 99.99% of APKs have a zero-length comment field + maxCommentSize = min(UINT16_MAX_VALUE, fileSize - ZIP_EOCD_REC_MIN_SIZE) + maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize + logger.debug('maxCommentSize:%s', maxCommentSize) + logger.debug('maxEocdSize:%s', maxEocdSize) + bufOffsetInFile = fileSize - maxEocdSize + logger.debug('bufOffsetInFile:%s', bufOffsetInFile) + buf = mm[bufOffsetInFile:bufOffsetInFile+maxEocdSize] + # logger.debug('buf:%s',to_hex(buf)) + eocdOffsetInBuf = findEocdStartOffset(buf) + logger.debug('eocdOffsetInBuf:%s', eocdOffsetInBuf) + if eocdOffsetInBuf != -1: + return bufOffsetInFile+eocdOffsetInBuf, buf[eocdOffsetInBuf:] + + +def findEocdStartOffset(buf): + archiveSize = len(buf) + logger.debug('archiveSize:%s', archiveSize) + maxCommentLength = min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE) + logger.debug('maxCommentLength:%s', maxCommentLength) + eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE + logger.debug('eocdWithEmptyCommentStartPosition:%s', eocdWithEmptyCommentStartPosition) + expectedCommentLength = 0 + eocdOffsetInBuf = -1 + while expectedCommentLength <= maxCommentLength: + eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength + logger.debug('expectedCommentLength:%s', expectedCommentLength) + # logger.debug('eocdStartPos:%s', eocdStartPos) + # logger.debug('eocdStart:%s', to_hex(buf[eocdStartPos:eocdStartPos+4])) + seg = ByteDecoder(buf).getInt(eocdStartPos) + logger.debug('seg:%s', hex(seg)) + if seg == ZIP_EOCD_REC_SIG: + actualCommentLength = ByteDecoder(buf).getUShort(eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET) + logger.debug('actualCommentLength:%s', actualCommentLength) + if actualCommentLength == expectedCommentLength: + logger.debug('found eocdStartPos:%s', eocdStartPos) + return eocdStartPos + expectedCommentLength += 1 + return -1 + + +def to_hex(s): + return " ".join("{:02x}".format(ord(c)) for c in s) if s else "" if __name__ == '__main__': - show_info(APK) + prog = os.path.basename(sys.argv[0]) + if len(sys.argv) < 2: + print('Usage: {} app.apk'.format(prog)) + sys.exit(1) + try: + apk = os.path.abspath(sys.argv[1]) + print('File: \t{}'.format(os.path.basename(apk))) + channel = getChannel(apk) + print('Channel: \t{}'.format(channel)) + except Exception as e: + print("Error:", e) From d3d60da0ea0aaddab4591246d2848b486a03daa3 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 7 Jun 2017 15:44:27 +0800 Subject: [PATCH 26/67] remove unused, simplify apkinfo.py --- tools/apkinfo.py | 4057 +--------------------------------------------- 1 file changed, 10 insertions(+), 4047 deletions(-) diff --git a/tools/apkinfo.py b/tools/apkinfo.py index d9f6e0c..f99f722 100644 --- a/tools/apkinfo.py +++ b/tools/apkinfo.py @@ -33,30 +33,14 @@ NS_ANDROID_URI = 'http://schemas.android.com/apk/res/android' -# 0: chilkat -# 1: default python zipfile module -# 2: patch zipfile module ZIPMODULE = 1 -if sys.hexversion < 0x2070000: - try: - import chilkat - ZIPMODULE = 0 - # UNLOCK : change it with your valid key ! - try: - CHILKAT_KEY = read("key.txt") - except Exception: - CHILKAT_KEY = "testme" - - except ImportError: - ZIPMODULE = 1 -else: - ZIPMODULE = 1 def read(filename, binary=True): with open(filename, 'rb' if binary else 'r') as f: return f.read() + def sign_apk(filename, keystore, storepass): from subprocess import Popen, PIPE, STDOUT compile = Popen([CONF["PATH_JARSIGNER"], "-sigalg", "MD5withRSA", @@ -66,61 +50,6 @@ def sign_apk(filename, keystore, storepass): stderr=STDOUT) stdout, stderr = compile.communicate() -################################################### CHILKAT ZIP FORMAT ##################################################### -class ChilkatZip(object): - - def __init__(self, raw): - self.files = [] - self.zip = chilkat.CkZip() - - self.zip.UnlockComponent(CHILKAT_KEY) - - self.zip.OpenFromMemory(raw, len(raw)) - - filename = chilkat.CkString() - e = self.zip.FirstEntry() - while e is not None: - e.get_FileName(filename) - self.files.append(filename.getString()) - e = e.NextEntry() - - def delete(self, patterns): - el = [] - - filename = chilkat.CkString() - e = self.zip.FirstEntry() - while e is not None: - e.get_FileName(filename) - - if re.match(patterns, filename.getString()) != None: - el.append(e) - e = e.NextEntry() - - for i in el: - self.zip.DeleteEntry(i) - - def remplace_file(self, filename, buff): - entry = self.zip.GetEntryByName(filename) - if entry is not None: - obj = chilkat.CkByteData() - obj.append2(buff, len(buff)) - return entry.ReplaceData(obj) - return False - - def write(self): - obj = chilkat.CkByteData() - self.zip.WriteToMemory(obj) - return obj.getBytes() - - def namelist(self): - return self.files - - def read(self, elem): - e = self.zip.GetEntryByName(elem) - s = chilkat.CkByteData() - - e.Inflate(s) - return s.getBytes() class Error(Exception): """Base class for exceptions in this module.""" @@ -212,53 +141,8 @@ def __init__(self, ] = self.xml[i].documentElement.getAttributeNS( NS_ANDROID_URI, "versionName") - for item in self.xml[i].getElementsByTagName('uses-permission'): - self.permissions.append(str(item.getAttributeNS( - NS_ANDROID_URI, "name"))) - - # getting details of the declared permissions - for d_perm_item in self.xml[i].getElementsByTagName('permission'): - d_perm_name = self._get_res_string_value(str( - d_perm_item.getAttributeNS(NS_ANDROID_URI, "name"))) - d_perm_label = self._get_res_string_value(str( - d_perm_item.getAttributeNS(NS_ANDROID_URI, - "label"))) - d_perm_description = self._get_res_string_value(str( - d_perm_item.getAttributeNS(NS_ANDROID_URI, - "description"))) - d_perm_permissionGroup = self._get_res_string_value(str( - d_perm_item.getAttributeNS(NS_ANDROID_URI, - "permissionGroup"))) - d_perm_protectionLevel = self._get_res_string_value(str( - d_perm_item.getAttributeNS(NS_ANDROID_URI, - "protectionLevel"))) - - d_perm_details = { - "label": d_perm_label, - "description": d_perm_description, - "permissionGroup": d_perm_permissionGroup, - "protectionLevel": d_perm_protectionLevel, - } - self.declared_permissions[d_perm_name] = d_perm_details - self.valid_apk = True - self.get_files_types() - - def _get_res_string_value(self, string): - if not string.startswith('@string/'): - return string - string_key = string[9:] - - res_parser = self.get_android_resources() - string_value = '' - for package_name in res_parser.get_packages_names(): - extracted_values = res_parser.get_string(package_name, string_key) - if extracted_values: - string_value = extracted_values[1] - break - return string_value - def get_AndroidManifest(self): """ Return the Android Manifest XML file @@ -283,75 +167,6 @@ def get_filename(self): """ return self.filename - def get_app_name(self): - """ - Return the appname of the APK - - :rtype: string - """ - main_activity_name = self.get_main_activity() - - app_name = self.get_element('activity', 'label', name=main_activity_name) - if not app_name: - app_name = self.get_element('application', 'label') - - if app_name.startswith("@"): - res_id = int(app_name[1:], 16) - res_parser = self.get_android_resources() - - try: - app_name = res_parser.get_resolved_res_configs( - res_id, - ARSCResTableConfig.default_config())[0][1] - except Exception, e: - warning("Exception selecting app icon: %s", e) - app_name = "" - return app_name - - def get_app_icon(self, max_dpi=65536): - """ - Return the first non-greater density than max_dpi icon file name, - unless exact icon resolution is set in the manifest, in which case - return the exact file - - :rtype: string - """ - main_activity_name = self.get_main_activity() - - app_icon = self.get_element('activity', 'icon', name=main_activity_name) - - if not app_icon: - app_icon = self.get_element('application', 'icon') - - if not app_icon: - res_id = self.get_res_id_by_key(self.package, 'mipmap', 'ic_launcher') - if res_id: - app_icon = "@%x" % res_id - - if not app_icon: - res_id = self.get_res_id_by_key(self.package, 'drawable', 'ic_launcher') - if res_id: - app_icon = "@%x" % res_id - - if app_icon.startswith("@"): - res_id = int(app_icon[1:], 16) - res_parser = self.get_android_resources() - candidates = res_parser.get_resolved_res_configs(res_id) - - app_icon = None - current_dpi = 0 - - try: - for config, file_name in candidates: - dpi = config.get_density() - if dpi <= max_dpi and dpi > current_dpi: - app_icon = file_name - current_dpi = dpi - except Exception, e: - warning("Exception selecting app icon: %s", e) - - return app_icon - def get_package(self): """ Return the name of the package @@ -376,96 +191,6 @@ def get_version_name(self): """ return self.androidversion["Name"] - def get_files(self): - """ - Return the files inside the APK - - :rtype: a list of strings - """ - return self.zip.namelist() - - def get_files_types(self): - """ - Return the files inside the APK with their associated types (by using python-magic) - - :rtype: a dictionnary - """ - try: - import magic - except ImportError: - # no lib magic ! - for i in self.get_files(): - buffer = self.zip.read(i) - self.files_crc32[i] = crc32(buffer) - self.files[i] = "Unknown" - return self.files - - if self.files != {}: - return self.files - - builtin_magic = 0 - try: - getattr(magic, "MagicException") - except AttributeError: - builtin_magic = 1 - - if builtin_magic: - ms = magic.open(magic.MAGIC_NONE) - ms.load() - - for i in self.get_files(): - buffer = self.zip.read(i) - self.files[i] = ms.buffer(buffer) - if self.files[i] is None: - self.files[i] = "Unknown" - else: - self.files[i] = self._patch_magic(buffer, self.files[i]) - self.files_crc32[i] = crc32(buffer) - else: - m = magic.Magic(magic_file=self.magic_file) - for i in self.get_files(): - buffer = self.zip.read(i) - self.files[i] = m.from_buffer(buffer) - if self.files[i] is None: - self.files[i] = "Unknown" - else: - self.files[i] = self._patch_magic(buffer, self.files[i]) - self.files_crc32[i] = crc32(buffer) - - return self.files - - def _patch_magic(self, buffer, orig): - if ("Zip" in orig) or ("DBase" in orig): - val = is_android_raw(buffer) - if val == "APK": - if is_valid_android_raw(buffer): - return "Android application package file" - elif val == "AXML": - return "Android's binary XML" - - return orig - - def get_files_crc32(self): - if self.files_crc32 == {}: - self.get_files_types() - - return self.files_crc32 - - def get_files_information(self): - """ - Return the files inside the APK with their associated types and crc32 - - :rtype: string, string, int - """ - if self.files == {}: - self.get_files_types() - - for i in self.get_files(): - try: - yield i, self.files[i], self.files_crc32[i] - except KeyError: - yield i, "", "" - def get_raw(self): """ Return raw bytes of the APK @@ -474,44 +199,6 @@ def get_raw(self): """ return self.__raw - def get_file(self, filename): - """ - Return the raw data of the specified filename - - :rtype: string - """ - try: - return self.zip.read(filename) - except KeyError: - raise FileNotPresent(filename) - - def get_dex(self): - """ - Return the raw data of the classes dex file - - :rtype: a string - """ - try: - return self.get_file("classes.dex") - except FileNotPresent: - return "" - - def get_all_dex(self): - """ - Return the raw data of all classes dex files - - :rtype: a generator - """ - try: - yield self.get_file("classes.dex") - - # Multidex support - basename = "classes%d.dex" - for i in xrange(2, sys.maxint): - yield self.get_file(basename % i) - except FileNotPresent: - pass - def get_elements(self, tag_name, attribute): """ Return elements in xml files which match with the tag name and the specific attribute @@ -569,150 +256,6 @@ def get_element(self, tag_name, attribute, **attribute_filter): return value return None - def get_main_activity(self): - """ - Return the name of the main activity - - :rtype: string - """ - x = set() - y = set() - - for i in self.xml: - for item in self.xml[i].getElementsByTagName("activity"): - for sitem in item.getElementsByTagName("action"): - val = sitem.getAttributeNS(NS_ANDROID_URI, "name") - if val == "android.intent.action.MAIN": - x.add(item.getAttributeNS(NS_ANDROID_URI, "name")) - - for sitem in item.getElementsByTagName("category"): - val = sitem.getAttributeNS(NS_ANDROID_URI, "name") - if val == "android.intent.category.LAUNCHER": - y.add(item.getAttributeNS(NS_ANDROID_URI, "name")) - - z = x.intersection(y) - if len(z) > 0: - return self.format_value(z.pop()) - return None - - def get_activities(self): - """ - Return the android:name attribute of all activities - - :rtype: a list of string - """ - return self.get_elements("activity", "name") - - def get_services(self): - """ - Return the android:name attribute of all services - - :rtype: a list of string - """ - return self.get_elements("service", "name") - - def get_receivers(self): - """ - Return the android:name attribute of all receivers - - :rtype: a list of string - """ - return self.get_elements("receiver", "name") - - def get_providers(self): - """ - Return the android:name attribute of all providers - - :rtype: a list of string - """ - return self.get_elements("provider", "name") - - def get_intent_filters(self, category, name): - d = {} - - d["action"] = [] - d["category"] = [] - - for i in self.xml: - for item in self.xml[i].getElementsByTagName(category): - if self.format_value( - item.getAttributeNS(NS_ANDROID_URI, "name") - ) == name: - for sitem in item.getElementsByTagName("intent-filter"): - for ssitem in sitem.getElementsByTagName("action"): - if ssitem.getAttributeNS(NS_ANDROID_URI, "name") \ - not in d["action"]: - d["action"].append(ssitem.getAttributeNS( - NS_ANDROID_URI, "name")) - for ssitem in sitem.getElementsByTagName("category"): - if ssitem.getAttributeNS(NS_ANDROID_URI, "name") \ - not in d["category"]: - d["category"].append(ssitem.getAttributeNS( - NS_ANDROID_URI, "name")) - - if not d["action"]: - del d["action"] - - if not d["category"]: - del d["category"] - - return d - - def get_permissions(self): - """ - Return permissions - - :rtype: list of string - """ - return self.permissions - - def get_details_permissions(self): - """ - Return permissions with details - - :rtype: list of string - """ - l = {} - - for i in self.permissions: - perm = i - pos = i.rfind(".") - - if pos != -1: - perm = i[pos + 1:] - - try: - l[i] = DVM_PERMISSIONS["MANIFEST_PERMISSION"][perm] - except KeyError: - l[i] = ["normal", "Unknown permission from android reference", - "Unknown permission from android reference"] - - return l - - def get_requested_permissions(self): - """ - Returns all requested permissions. - - :rtype: list of strings - """ - return self.permissions - - def get_declared_permissions(self): - ''' - Returns list of the declared permissions. - - :rtype: list of strings - ''' - return self.declared_permissions.keys() - - def get_declared_permissions_details(self): - ''' - Returns declared permissions with the details. - - :rtype: dict - ''' - return self.declared_permissions - def get_max_sdk_version(self): """ Return the android:maxSdkVersion attribute @@ -737,56 +280,6 @@ def get_target_sdk_version(self): """ return self.get_element("uses-sdk", "targetSdkVersion") - def get_libraries(self): - """ - Return the android:name attributes for libraries - - :rtype: list - """ - return self.get_elements("uses-library", "name") - - def get_certificate(self, filename): - """ - Return a certificate object by giving the name in the apk file - """ - import chilkat - - cert = chilkat.CkCert() - f = self.get_file(filename) - data = chilkat.CkByteData() - data.append2(f, len(f)) - success = cert.LoadFromBinary(data) - return success, cert - - def new_zip(self, filename, deleted_files=None, new_files={}): - """ - Create a new zip file - - :param filename: the output filename of the zip - :param deleted_files: a regex pattern to remove specific file - :param new_files: a dictionnary of new files - - :type filename: string - :type deleted_files: None or a string - :type new_files: a dictionnary (key:filename, value:content of the file) - """ - if self.zipmodule == 2: - from androguard.patch import zipfile - zout = zipfile.ZipFile(filename, 'w') - else: - import zipfile - zout = zipfile.ZipFile(filename, 'w') - - for item in self.zip.infolist(): - if deleted_files is not None: - if re.match(deleted_files, item.filename) == None: - if item.filename in new_files: - zout.writestr(item, new_files[item.filename]) - else: - buffer = self.zip.read(item.filename) - zout.writestr(item, buffer) - zout.close() - def get_android_manifest_axml(self): """ Return the :class:`AXMLPrinter` object which corresponds to the AndroidManifest.xml file @@ -809,116 +302,14 @@ def get_android_manifest_xml(self): except KeyError: return None - def get_android_resources(self): - """ - Return the :class:`ARSCParser` object which corresponds to the resources.arsc file - - :rtype: :class:`ARSCParser` - """ - try: - return self.arsc["resources.arsc"] - except KeyError: - self.arsc["resources.arsc"] = ARSCParser(self.zip.read( - "resources.arsc")) - return self.arsc["resources.arsc"] - - def get_signature_name(self): - """ - Return the name of the first signature file found. - """ - return self.get_signature_names()[0] - - def get_signature_names(self): - """ - Return a list of the signature file names. - """ - signature_expr = re.compile("^(META-INF/)(.*)(\.RSA|\.EC|\.DSA)$") - signatures = [] - - for i in self.get_files(): - if signature_expr.search(i): - signatures.append(i) - - if len(signatures) > 0: - return signatures - - return None - - def get_signature(self): - """ - Return the data of the first signature file found. - """ - return self.get_signatures()[0] - - def get_signatures(self): - """ - Return a list of the data of the signature files. - """ - signature_expr = re.compile("^(META-INF/)(.*)(\.RSA|\.EC|\.DSA)$") - signature_datas = [] - - for i in self.get_files(): - if signature_expr.search(i): - signature_datas.append(self.get_file(i)) - - if len(signature_datas) > 0: - return signature_datas - - return None - def show(self): - self.get_files_types() - - print "FILES: " - for i in self.get_files(): - try: - print "\t", i, self.files[i], "%x" % self.files_crc32[i] - except KeyError: - print "\t", i, "%x" % self.files_crc32[i] - - print "DECLARED PERMISSIONS:" - declared_permissions = self.get_declared_permissions() - for i in declared_permissions: - print "\t", i - - print "REQUESTED PERMISSIONS:" - requested_permissions = self.get_requested_permissions() - for i in requested_permissions: - print "\t", i - - print "MAIN ACTIVITY: ", self.get_main_activity() - - print "ACTIVITIES: " - activities = self.get_activities() - for i in activities: - filters = self.get_intent_filters("activity", i) - print "\t", i, filters or "" - - print "SERVICES: " - services = self.get_services() - for i in services: - filters = self.get_intent_filters("service", i) - print "\t", i, filters or "" - - print "RECEIVERS: " - receivers = self.get_receivers() - for i in receivers: - filters = self.get_intent_filters("receiver", i) - print "\t", i, filters or "" - - print "PROVIDERS: ", self.get_providers() - - -def show_Certificate(cert): - print "Issuer: C=%s, CN=%s, DN=%s, E=%s, L=%s, O=%s, OU=%s, S=%s" % ( - cert.issuerC(), cert.issuerCN(), cert.issuerDN(), cert.issuerE(), - cert.issuerL(), cert.issuerO(), cert.issuerOU(), cert.issuerS()) - print "Subject: C=%s, CN=%s, DN=%s, E=%s, L=%s, O=%s, OU=%s, S=%s" % ( - cert.subjectC(), cert.subjectCN(), cert.subjectDN(), cert.subjectE(), - cert.subjectL(), cert.subjectO(), cert.subjectOU(), cert.subjectS()) + + print "PACKAGE: ", self.get_package() + print "VERSION NAME:", self.get_version_name() + print "VERSION CODE:", self.get_version_code() ################################## AXML FORMAT ######################################## -# Translated from +# Translated from # http://code.google.com/p/android4me/source/browse/src/android/content/res/AXmlResourceParser.java UTF8_FLAG = 0x00000100 @@ -1088,6 +479,7 @@ def show(self): CHUNK_XML_TEXT = 0x00100104 CHUNK_XML_LAST = 0x00100104 + class AXMLParser(object): def __init__(self, raw_buff): @@ -1252,7 +644,7 @@ def getPrefix(self): def getName(self): if self.m_name == -1 or (self.m_event != START_TAG and - self.m_event != END_TAG): + self.m_event != END_TAG): return u'' return self.sb.getString(self.m_name) @@ -1347,7 +739,7 @@ def getAttributeValue(self, index): # WIP return "" -### resource constants +# resource constants TYPE_ATTRIBUTE = 2 TYPE_DIMENSION = 5 @@ -1356,10 +748,6 @@ def getAttributeValue(self, index): TYPE_FLOAT = 4 TYPE_FRACTION = 6 TYPE_INT_BOOLEAN = 18 -TYPE_INT_COLOR_ARGB4 = 30 -TYPE_INT_COLOR_ARGB8 = 28 -TYPE_INT_COLOR_RGB4 = 31 -TYPE_INT_COLOR_RGB8 = 29 TYPE_INT_DEC = 16 TYPE_INT_HEX = 17 TYPE_LAST_COLOR_INT = 31 @@ -1374,10 +762,6 @@ def getAttributeValue(self, index): TYPE_FLOAT: "float", TYPE_FRACTION: "fraction", TYPE_INT_BOOLEAN: "int_boolean", - TYPE_INT_COLOR_ARGB4: "int_color_argb4", - TYPE_INT_COLOR_ARGB8: "int_color_argb8", - TYPE_INT_COLOR_RGB4: "int_color_rgb4", - TYPE_INT_COLOR_RGB8: "int_color_rgb8", TYPE_INT_DEC: "int_dec", TYPE_INT_HEX: "int_hex", TYPE_NULL: "null", @@ -1507,822 +891,6 @@ def getAttributeValue(self, index): return format_value(_type, _data, lambda _: self.axml.getAttributeValue(index)) -RES_NULL_TYPE = 0x0000 -RES_STRING_POOL_TYPE = 0x0001 -RES_TABLE_TYPE = 0x0002 -RES_XML_TYPE = 0x0003 - -# Chunk types in RES_XML_TYPE -RES_XML_FIRST_CHUNK_TYPE = 0x0100 -RES_XML_START_NAMESPACE_TYPE = 0x0100 -RES_XML_END_NAMESPACE_TYPE = 0x0101 -RES_XML_START_ELEMENT_TYPE = 0x0102 -RES_XML_END_ELEMENT_TYPE = 0x0103 -RES_XML_CDATA_TYPE = 0x0104 -RES_XML_LAST_CHUNK_TYPE = 0x017f - -# This contains a uint32_t array mapping strings in the string -# pool back to resource identifiers. It is optional. -RES_XML_RESOURCE_MAP_TYPE = 0x0180 - -# Chunk types in RES_TABLE_TYPE -RES_TABLE_PACKAGE_TYPE = 0x0200 -RES_TABLE_TYPE_TYPE = 0x0201 -RES_TABLE_TYPE_SPEC_TYPE = 0x0202 - -ACONFIGURATION_MCC = 0x0001 -ACONFIGURATION_MNC = 0x0002 -ACONFIGURATION_LOCALE = 0x0004 -ACONFIGURATION_TOUCHSCREEN = 0x0008 -ACONFIGURATION_KEYBOARD = 0x0010 -ACONFIGURATION_KEYBOARD_HIDDEN = 0x0020 -ACONFIGURATION_NAVIGATION = 0x0040 -ACONFIGURATION_ORIENTATION = 0x0080 -ACONFIGURATION_DENSITY = 0x0100 -ACONFIGURATION_SCREEN_SIZE = 0x0200 -ACONFIGURATION_VERSION = 0x0400 -ACONFIGURATION_SCREEN_LAYOUT = 0x0800 -ACONFIGURATION_UI_MODE = 0x1000 - - -class ARSCParser(object): - - def __init__(self, raw_buff): - self.analyzed = False - self.buff = BuffHandle(raw_buff) - - self.header = ARSCHeader(self.buff) - self.packageCount = unpack('> 24) & 0xFF), - ((entry_data >> 16) & 0xFF), - ((entry_data >> 8) & 0xFF), - (entry_data & 0xFF)) - ] - - def get_resource_dimen(self, ate): - try: - return [ - ate.get_value(), "%s%s" % ( - complexToFloat(ate.key.get_data()), - DIMENSION_UNITS[ate.key.get_data() & COMPLEX_UNIT_MASK]) - ] - except IndexError: - debug("Out of range dimension unit index for %s: %s" % ( - complexToFloat(ate.key.get_data()), - ate.key.get_data() & COMPLEX_UNIT_MASK)) - return [ate.get_value(), ate.key.get_data()] - - # FIXME - def get_resource_style(self, ate): - return ["", ""] - - def get_packages_names(self): - return self.packages.keys() - - def get_locales(self, package_name): - self._analyse() - return self.values[package_name].keys() - - def get_types(self, package_name, locale): - self._analyse() - return self.values[package_name][locale].keys() - - def get_public_resources(self, package_name, locale='\x00\x00'): - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["public"]: - buff += '\n' % ( - i[0], i[1], i[2]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_string_resources(self, package_name, locale='\x00\x00'): - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["string"]: - buff += '%s\n' % (i[0], i[1]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_strings_resources(self): - self._analyse() - - buff = '\n' - - buff += "\n" - for package_name in self.get_packages_names(): - buff += "\n" % package_name - - for locale in self.get_locales(package_name): - buff += "\n" % repr(locale) - - buff += '\n' - try: - for i in self.values[package_name][locale]["string"]: - buff += '%s\n' % (i[0], i[1]) - except KeyError: - pass - - buff += '\n' - buff += '\n' - - buff += "\n" - - buff += "\n" - - return buff.encode('utf-8') - - def get_id_resources(self, package_name, locale='\x00\x00'): - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["id"]: - if len(i) == 1: - buff += '\n' % (i[0]) - else: - buff += '%s\n' % (i[0], - i[1]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_bool_resources(self, package_name, locale='\x00\x00'): - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["bool"]: - buff += '%s\n' % (i[0], i[1]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_integer_resources(self, package_name, locale='\x00\x00'): - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["integer"]: - buff += '%s\n' % (i[0], i[1]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_color_resources(self, package_name, locale='\x00\x00'): - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["color"]: - buff += '%s\n' % (i[0], i[1]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_dimen_resources(self, package_name, locale='\x00\x00'): - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["dimen"]: - buff += '%s\n' % (i[0], i[1]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_id(self, package_name, rid, locale='\x00\x00'): - self._analyse() - - try: - for i in self.values[package_name][locale]["public"]: - if i[2] == rid: - return i - except KeyError: - return None - - class ResourceResolver(object): - def __init__(self, android_resources, config=None): - self.resources = android_resources - self.wanted_config = config - - def resolve(self, res_id): - result = [] - self._resolve_into_result(result, res_id, self.wanted_config) - return result - - def _resolve_into_result(self, result, res_id, config): - configs = self.resources.get_res_configs(res_id, config) - if configs: - for config, ate in configs: - self.put_ate_value(result, ate, config) - - def put_ate_value(self, result, ate, config): - if ate.is_complex(): - complex_array = [] - result.append(config, complex_array) - for _, item in ate.item.items: - self.put_item_value(complex_array, item, config, complex_=True) - else: - self.put_item_value(result, ate.key, config, complex_=False) - - def put_item_value(self, result, item, config, complex_): - if item.is_reference(): - res_id = item.get_data() - if res_id: - self._resolve_into_result( - result, - item.get_data(), - self.wanted_config) - else: - if complex_: - result.append(item.format_value()) - else: - result.append((config, item.format_value())) - - def get_resolved_res_configs(self, rid, config=None): - resolver = ARSCParser.ResourceResolver(self, config) - return resolver.resolve(rid) - - def get_res_configs(self, rid, config=None): - self._analyse() - - if not rid: - raise ValueError("'rid' should be set") - - try: - res_options = self.resource_values[rid] - if len(res_options) > 1 and config: - return [( - config, - res_options[config])] - else: - return res_options.items() - - except KeyError: - return [] - - def get_string(self, package_name, name, locale='\x00\x00'): - self._analyse() - - try: - for i in self.values[package_name][locale]["string"]: - if i[0] == name: - return i - except KeyError: - return None - - def get_res_id_by_key(self, package_name, resource_type, key): - try: - return self.resource_keys[package_name][resource_type][key] - except KeyError: - return None - - def get_items(self, package_name): - self._analyse() - return self.packages[package_name] - - def get_type_configs(self, package_name, type_name=None): - if package_name is None: - package_name = self.get_packages_names()[0] - result = collections.defaultdict(list) - - for res_type, configs in self.resource_configs[package_name].items(): - if res_type.get_package_name() == package_name and ( - type_name is None or res_type.get_type() == type_name): - result[res_type.get_type()].extend(configs) - - return result - - -class PackageContext(object): - - def __init__(self, current_package, stringpool_main, mTableStrings, - mKeyStrings): - self.stringpool_main = stringpool_main - self.mTableStrings = mTableStrings - self.mKeyStrings = mKeyStrings - self.current_package = current_package - - def get_mResId(self): - return self.current_package.mResId - - def set_mResId(self, mResId): - self.current_package.mResId = mResId - - def get_package_name(self): - return self.current_package.get_name() - - -class ARSCHeader(object): - - def __init__(self, buff): - self.start = buff.get_idx() - self.type = unpack('= 32: - self.screenConfig = unpack('= 36: - self.screenSizeDp = unpack(' 0: - info("Skipping padding bytes!") - self.padding = buff.read(self.exceedingSize) - else: - self.start = 0 - self.size = 0 - self.imsi = \ - ((kwargs.pop('mcc', 0) & 0xffff) << 0) + \ - ((kwargs.pop('mnc', 0) & 0xffff) << 16) - - self.locale = 0 - for char_ix, char in kwargs.pop('locale', "")[0:4]: - self.locale += (ord(char) << (char_ix * 8)) - - self.screenType = \ - ((kwargs.pop('orientation', 0) & 0xff) << 0) + \ - ((kwargs.pop('touchscreen', 0) & 0xff) << 8) + \ - ((kwargs.pop('density', 0) & 0xffff) << 16) - - self.input = \ - ((kwargs.pop('keyboard', 0) & 0xff) << 0) + \ - ((kwargs.pop('navigation', 0) & 0xff) << 8) + \ - ((kwargs.pop('inputFlags', 0) & 0xff) << 16) + \ - ((kwargs.pop('inputPad0', 0) & 0xff) << 24) - - self.screenSize = \ - ((kwargs.pop('screenWidth', 0) & 0xffff) << 0) + \ - ((kwargs.pop('screenHeight', 0) & 0xffff) << 16) - - self.version = \ - ((kwargs.pop('sdkVersion', 0) & 0xffff) << 0) + \ - ((kwargs.pop('minorVersion', 0) & 0xffff) << 16) - - self.screenConfig = \ - ((kwargs.pop('screenLayout', 0) & 0xff) << 0) + \ - ((kwargs.pop('uiMode', 0) & 0xff) << 8) + \ - ((kwargs.pop('smallestScreenWidthDp', 0) & 0xffff) << 16) - - self.screenSizeDp = \ - ((kwargs.pop('screenWidthDp', 0) & 0xffff) << 0) + \ - ((kwargs.pop('screenHeightDp', 0) & 0xffff) << 16) - - self.exceedingSize = 0 - - def get_language(self): - x = self.locale & 0x0000ffff - return chr(x & 0x00ff) + chr((x & 0xff00) >> 8) - - def get_country(self): - x = (self.locale & 0xffff0000) >> 16 - return chr(x & 0x00ff) + chr((x & 0xff00) >> 8) - - def get_density(self): - x = ((self.screenType >> 16) & 0xffff) - return x - - def _get_tuple(self): - return ( - self.imsi, - self.locale, - self.screenType, - self.input, - self.screenSize, - self.version, - self.screenConfig, - self.screenSizeDp, - ) - - def __hash__(self): - return hash(self._get_tuple()) - - def __eq__(self, other): - return self._get_tuple() == other._get_tuple() - - def __repr__(self): - return repr(self._get_tuple()) - - -class ARSCResTableEntry(object): - - def __init__(self, buff, mResId, parent=None): - self.start = buff.get_idx() - self.mResId = mResId - self.parent = parent - self.size = unpack(' 0x7fffffff: l = (0x7fffffff & l) - 0x80000000 return l - - -def long2str(l): - """Convert an integer to a string.""" - if type(l) not in (types.IntType, types.LongType): - raise ValueError, 'the input must be an integer' - - if l < 0: - raise ValueError, 'the input must be greater than 0' - s = '' - while l: - s = s + chr(l & 255L) - l >>= 8 - - return s - - -def str2long(s): - """Convert a string to a long integer.""" - if type(s) not in (types.StringType, types.UnicodeType): - raise ValueError, 'the input must be a string' - - l = 0L - for i in s: - l <<= 8 - l |= ord(i) - - return l - - -def random_string(): - return random.choice(string.letters) + ''.join([random.choice( - string.letters + string.digits) for i in range(10 - 1)]) - - -def is_android(filename): - """Return the type of the file - - @param filename : the filename - @rtype : "APK", "DEX", "ELF", None - """ - if not filename: - return None - - val = None - with open(filename, "r") as fd: - f_bytes = fd.read() - val = is_android_raw(f_bytes) - - return val - - -def is_android_raw(raw): - val = None - - if raw[0:2] == "PK": - val = "APK" - elif raw[0:3] == "dex": - val = "DEX" - elif raw[0:3] == "dey": - val = "DEY" - elif raw[0:7] == "\x7fELF\x01\x01\x01": - val = "ELF" - elif raw[0:4] == "\x03\x00\x08\x00": - val = "AXML" - elif raw[0:4] == "\x02\x00\x0C\x00": - val = "ARSC" - elif ('AndroidManifest.xml' in raw and - 'META-INF/MANIFEST.MF' in raw): - val = "APK" - - return val - - -def is_valid_android_raw(raw): - return raw.find("classes.dex") != -1 - -# from scapy -log_andro = logging.getLogger("apkinfo") -console_handler = logging.StreamHandler() -console_handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) -log_andro.addHandler(console_handler) -log_runtime = logging.getLogger("apkinfo.runtime") # logs at runtime -log_interactive = logging.getLogger("andro.interactive") # logs in interactive functions -log_loading = logging.getLogger("apkinfo.loading") # logs when loading andro - - -def set_lazy(): - CONF["LAZY_ANALYSIS"] = True - - -def set_debug(): - log_andro.setLevel(logging.DEBUG) - - -def set_info(): - log_andro.setLevel(logging.INFO) - - -def get_debug(): - return log_andro.getEffectiveLevel() == logging.DEBUG - - -def warning(x): - log_runtime.warning(x) - import traceback - traceback.print_exc() - - -def error(x): - log_runtime.error(x) - raise () - - -def debug(x): - log_runtime.debug(x) - - -def info(x): - log_runtime.info(x) - - -def set_options(key, value): - CONF[key] = value - - -def save_to_disk(buff, output): - with open(output, "w") as fd: - fd.write(buff) - - -def rrmdir(directory): - for root, dirs, files in os.walk(directory, topdown=False): - for name in files: - os.remove(os.path.join(root, name)) - for name in dirs: - os.rmdir(os.path.join(root, name)) - os.rmdir(directory) - - -def make_color_tuple(color): - """ - turn something like "#000000" into 0,0,0 - or "#FFFFFF into "255,255,255" - """ - R = color[1:3] - G = color[3:5] - B = color[5:7] - - R = int(R, 16) - G = int(G, 16) - B = int(B, 16) - - return R, G, B - - -def interpolate_tuple(startcolor, goalcolor, steps): - """ - Take two RGB color sets and mix them over a specified number of steps. Return the list - """ - # white - - R = startcolor[0] - G = startcolor[1] - B = startcolor[2] - - targetR = goalcolor[0] - targetG = goalcolor[1] - targetB = goalcolor[2] - - DiffR = targetR - R - DiffG = targetG - G - DiffB = targetB - B - - buffer = [] - - for i in range(0, steps + 1): - iR = R + (DiffR * i / steps) - iG = G + (DiffG * i / steps) - iB = B + (DiffB * i / steps) - - hR = string.replace(hex(iR), "0x", "") - hG = string.replace(hex(iG), "0x", "") - hB = string.replace(hex(iB), "0x", "") - - if len(hR) == 1: - hR = "0" + hR - if len(hB) == 1: - hB = "0" + hB - - if len(hG) == 1: - hG = "0" + hG - - color = string.upper("#" + hR + hG + hB) - buffer.append(color) - - return buffer - - -def color_range(startcolor, goalcolor, steps): - """ - wrapper for interpolate_tuple that accepts colors as html ("#CCCCC" and such) - """ - start_tuple = make_color_tuple(startcolor) - goal_tuple = make_color_tuple(goalcolor) - - return interpolate_tuple(start_tuple, goal_tuple, steps) - - -### public resources -resources = { - 'style': { - 'Animation' : 16973824, - 'Animation.Activity' : 16973825, - 'Animation.Dialog' : 16973826, - 'Animation.InputMethod' : 16973910, - 'Animation.Toast' : 16973828, - 'Animation.Translucent' : 16973827, - 'DeviceDefault.ButtonBar' : 16974287, - 'DeviceDefault.ButtonBar.AlertDialog' : 16974288, - 'DeviceDefault.Light.ButtonBar' : 16974290, - 'DeviceDefault.Light.ButtonBar.AlertDialog' : 16974291, - 'DeviceDefault.Light.SegmentedButton' : 16974292, - 'DeviceDefault.SegmentedButton' : 16974289, - 'Holo.ButtonBar' : 16974053, - 'Holo.ButtonBar.AlertDialog' : 16974055, - 'Holo.Light.ButtonBar' : 16974054, - 'Holo.Light.ButtonBar.AlertDialog' : 16974056, - 'Holo.Light.SegmentedButton' : 16974058, - 'Holo.SegmentedButton' : 16974057, - 'MediaButton' : 16973879, - 'MediaButton.Ffwd' : 16973883, - 'MediaButton.Next' : 16973881, - 'MediaButton.Pause' : 16973885, - 'MediaButton.Play' : 16973882, - 'MediaButton.Previous' : 16973880, - 'MediaButton.Rew' : 16973884, - 'TextAppearance' : 16973886, - 'TextAppearance.DeviceDefault' : 16974253, - 'TextAppearance.DeviceDefault.DialogWindowTitle' : 16974264, - 'TextAppearance.DeviceDefault.Inverse' : 16974254, - 'TextAppearance.DeviceDefault.Large' : 16974255, - 'TextAppearance.DeviceDefault.Large.Inverse' : 16974256, - 'TextAppearance.DeviceDefault.Medium' : 16974257, - 'TextAppearance.DeviceDefault.Medium.Inverse' : 16974258, - 'TextAppearance.DeviceDefault.SearchResult.Subtitle' : 16974262, - 'TextAppearance.DeviceDefault.SearchResult.Title' : 16974261, - 'TextAppearance.DeviceDefault.Small' : 16974259, - 'TextAppearance.DeviceDefault.Small.Inverse' : 16974260, - 'TextAppearance.DeviceDefault.Widget' : 16974265, - 'TextAppearance.DeviceDefault.Widget.ActionBar.Menu' : 16974286, - 'TextAppearance.DeviceDefault.Widget.ActionBar.Subtitle' : 16974279, - 'TextAppearance.DeviceDefault.Widget.ActionBar.Subtitle.Inverse' : 16974283, - 'TextAppearance.DeviceDefault.Widget.ActionBar.Title' : 16974278, - 'TextAppearance.DeviceDefault.Widget.ActionBar.Title.Inverse' : 16974282, - 'TextAppearance.DeviceDefault.Widget.ActionMode.Subtitle' : 16974281, - 'TextAppearance.DeviceDefault.Widget.ActionMode.Subtitle.Inverse' : 16974285, - 'TextAppearance.DeviceDefault.Widget.ActionMode.Title' : 16974280, - 'TextAppearance.DeviceDefault.Widget.ActionMode.Title.Inverse' : 16974284, - 'TextAppearance.DeviceDefault.Widget.Button' : 16974266, - 'TextAppearance.DeviceDefault.Widget.DropDownHint' : 16974271, - 'TextAppearance.DeviceDefault.Widget.DropDownItem' : 16974272, - 'TextAppearance.DeviceDefault.Widget.EditText' : 16974274, - 'TextAppearance.DeviceDefault.Widget.IconMenu.Item' : 16974267, - 'TextAppearance.DeviceDefault.Widget.PopupMenu' : 16974275, - 'TextAppearance.DeviceDefault.Widget.PopupMenu.Large' : 16974276, - 'TextAppearance.DeviceDefault.Widget.PopupMenu.Small' : 16974277, - 'TextAppearance.DeviceDefault.Widget.TabWidget' : 16974268, - 'TextAppearance.DeviceDefault.Widget.TextView' : 16974269, - 'TextAppearance.DeviceDefault.Widget.TextView.PopupMenu' : 16974270, - 'TextAppearance.DeviceDefault.Widget.TextView.SpinnerItem' : 16974273, - 'TextAppearance.DeviceDefault.WindowTitle' : 16974263, - 'TextAppearance.DialogWindowTitle' : 16973889, - 'TextAppearance.Holo' : 16974075, - 'TextAppearance.Holo.DialogWindowTitle' : 16974103, - 'TextAppearance.Holo.Inverse' : 16974076, - 'TextAppearance.Holo.Large' : 16974077, - 'TextAppearance.Holo.Large.Inverse' : 16974078, - 'TextAppearance.Holo.Medium' : 16974079, - 'TextAppearance.Holo.Medium.Inverse' : 16974080, - 'TextAppearance.Holo.SearchResult.Subtitle' : 16974084, - 'TextAppearance.Holo.SearchResult.Title' : 16974083, - 'TextAppearance.Holo.Small' : 16974081, - 'TextAppearance.Holo.Small.Inverse' : 16974082, - 'TextAppearance.Holo.Widget' : 16974085, - 'TextAppearance.Holo.Widget.ActionBar.Menu' : 16974112, - 'TextAppearance.Holo.Widget.ActionBar.Subtitle' : 16974099, - 'TextAppearance.Holo.Widget.ActionBar.Subtitle.Inverse' : 16974109, - 'TextAppearance.Holo.Widget.ActionBar.Title' : 16974098, - 'TextAppearance.Holo.Widget.ActionBar.Title.Inverse' : 16974108, - 'TextAppearance.Holo.Widget.ActionMode.Subtitle' : 16974101, - 'TextAppearance.Holo.Widget.ActionMode.Subtitle.Inverse' : 16974111, - 'TextAppearance.Holo.Widget.ActionMode.Title' : 16974100, - 'TextAppearance.Holo.Widget.ActionMode.Title.Inverse' : 16974110, - 'TextAppearance.Holo.Widget.Button' : 16974086, - 'TextAppearance.Holo.Widget.DropDownHint' : 16974091, - 'TextAppearance.Holo.Widget.DropDownItem' : 16974092, - 'TextAppearance.Holo.Widget.EditText' : 16974094, - 'TextAppearance.Holo.Widget.IconMenu.Item' : 16974087, - 'TextAppearance.Holo.Widget.PopupMenu' : 16974095, - 'TextAppearance.Holo.Widget.PopupMenu.Large' : 16974096, - 'TextAppearance.Holo.Widget.PopupMenu.Small' : 16974097, - 'TextAppearance.Holo.Widget.TabWidget' : 16974088, - 'TextAppearance.Holo.Widget.TextView' : 16974089, - 'TextAppearance.Holo.Widget.TextView.PopupMenu' : 16974090, - 'TextAppearance.Holo.Widget.TextView.SpinnerItem' : 16974093, - 'TextAppearance.Holo.WindowTitle' : 16974102, - 'TextAppearance.Inverse' : 16973887, - 'TextAppearance.Large' : 16973890, - 'TextAppearance.Large.Inverse' : 16973891, - 'TextAppearance.Material' : 16974317, - 'TextAppearance.Material.Body1' : 16974320, - 'TextAppearance.Material.Body2' : 16974319, - 'TextAppearance.Material.Button' : 16974318, - 'TextAppearance.Material.Caption' : 16974321, - 'TextAppearance.Material.DialogWindowTitle' : 16974322, - 'TextAppearance.Material.Display1' : 16974326, - 'TextAppearance.Material.Display2' : 16974325, - 'TextAppearance.Material.Display3' : 16974324, - 'TextAppearance.Material.Display4' : 16974323, - 'TextAppearance.Material.Headline' : 16974327, - 'TextAppearance.Material.Inverse' : 16974328, - 'TextAppearance.Material.Large' : 16974329, - 'TextAppearance.Material.Large.Inverse' : 16974330, - 'TextAppearance.Material.Medium' : 16974331, - 'TextAppearance.Material.Medium.Inverse' : 16974332, - 'TextAppearance.Material.Menu' : 16974333, - 'TextAppearance.Material.Notification' : 16974334, - 'TextAppearance.Material.Notification.Emphasis' : 16974335, - 'TextAppearance.Material.Notification.Info' : 16974336, - 'TextAppearance.Material.Notification.Line2' : 16974337, - 'TextAppearance.Material.Notification.Time' : 16974338, - 'TextAppearance.Material.Notification.Title' : 16974339, - 'TextAppearance.Material.SearchResult.Subtitle' : 16974340, - 'TextAppearance.Material.SearchResult.Title' : 16974341, - 'TextAppearance.Material.Small' : 16974342, - 'TextAppearance.Material.Small.Inverse' : 16974343, - 'TextAppearance.Material.Subhead' : 16974344, - 'TextAppearance.Material.Title' : 16974345, - 'TextAppearance.Material.Widget' : 16974347, - 'TextAppearance.Material.Widget.ActionBar.Menu' : 16974348, - 'TextAppearance.Material.Widget.ActionBar.Subtitle' : 16974349, - 'TextAppearance.Material.Widget.ActionBar.Subtitle.Inverse' : 16974350, - 'TextAppearance.Material.Widget.ActionBar.Title' : 16974351, - 'TextAppearance.Material.Widget.ActionBar.Title.Inverse' : 16974352, - 'TextAppearance.Material.Widget.ActionMode.Subtitle' : 16974353, - 'TextAppearance.Material.Widget.ActionMode.Subtitle.Inverse' : 16974354, - 'TextAppearance.Material.Widget.ActionMode.Title' : 16974355, - 'TextAppearance.Material.Widget.ActionMode.Title.Inverse' : 16974356, - 'TextAppearance.Material.Widget.Button' : 16974357, - 'TextAppearance.Material.Widget.DropDownHint' : 16974358, - 'TextAppearance.Material.Widget.DropDownItem' : 16974359, - 'TextAppearance.Material.Widget.EditText' : 16974360, - 'TextAppearance.Material.Widget.IconMenu.Item' : 16974361, - 'TextAppearance.Material.Widget.PopupMenu' : 16974362, - 'TextAppearance.Material.Widget.PopupMenu.Large' : 16974363, - 'TextAppearance.Material.Widget.PopupMenu.Small' : 16974364, - 'TextAppearance.Material.Widget.TabWidget' : 16974365, - 'TextAppearance.Material.Widget.TextView' : 16974366, - 'TextAppearance.Material.Widget.TextView.PopupMenu' : 16974367, - 'TextAppearance.Material.Widget.TextView.SpinnerItem' : 16974368, - 'TextAppearance.Material.Widget.Toolbar.Subtitle' : 16974369, - 'TextAppearance.Material.Widget.Toolbar.Title' : 16974370, - 'TextAppearance.Material.WindowTitle' : 16974346, - 'TextAppearance.Medium' : 16973892, - 'TextAppearance.Medium.Inverse' : 16973893, - 'TextAppearance.Small' : 16973894, - 'TextAppearance.Small.Inverse' : 16973895, - 'TextAppearance.StatusBar.EventContent' : 16973927, - 'TextAppearance.StatusBar.EventContent.Title' : 16973928, - 'TextAppearance.StatusBar.Icon' : 16973926, - 'TextAppearance.StatusBar.Title' : 16973925, - 'TextAppearance.SuggestionHighlight' : 16974104, - 'TextAppearance.Theme' : 16973888, - 'TextAppearance.Theme.Dialog' : 16973896, - 'TextAppearance.Widget' : 16973897, - 'TextAppearance.Widget.Button' : 16973898, - 'TextAppearance.Widget.DropDownHint' : 16973904, - 'TextAppearance.Widget.DropDownItem' : 16973905, - 'TextAppearance.Widget.EditText' : 16973900, - 'TextAppearance.Widget.IconMenu.Item' : 16973899, - 'TextAppearance.Widget.PopupMenu.Large' : 16973952, - 'TextAppearance.Widget.PopupMenu.Small' : 16973953, - 'TextAppearance.Widget.TabWidget' : 16973901, - 'TextAppearance.Widget.TextView' : 16973902, - 'TextAppearance.Widget.TextView.PopupMenu' : 16973903, - 'TextAppearance.Widget.TextView.SpinnerItem' : 16973906, - 'TextAppearance.WindowTitle' : 16973907, - 'Theme' : 16973829, - 'ThemeOverlay' : 16974407, - 'ThemeOverlay.Material' : 16974408, - 'ThemeOverlay.Material.ActionBar' : 16974409, - 'ThemeOverlay.Material.Dark' : 16974411, - 'ThemeOverlay.Material.Dark.ActionBar' : 16974412, - 'ThemeOverlay.Material.Light' : 16974410, - 'Theme.Black' : 16973832, - 'Theme.Black.NoTitleBar' : 16973833, - 'Theme.Black.NoTitleBar.Fullscreen' : 16973834, - 'Theme.DeviceDefault' : 16974120, - 'Theme.DeviceDefault.Dialog' : 16974126, - 'Theme.DeviceDefault.DialogWhenLarge' : 16974134, - 'Theme.DeviceDefault.DialogWhenLarge.NoActionBar' : 16974135, - 'Theme.DeviceDefault.Dialog.MinWidth' : 16974127, - 'Theme.DeviceDefault.Dialog.NoActionBar' : 16974128, - 'Theme.DeviceDefault.Dialog.NoActionBar.MinWidth' : 16974129, - 'Theme.DeviceDefault.InputMethod' : 16974142, - 'Theme.DeviceDefault.Light' : 16974123, - 'Theme.DeviceDefault.Light.DarkActionBar' : 16974143, - 'Theme.DeviceDefault.Light.Dialog' : 16974130, - 'Theme.DeviceDefault.Light.DialogWhenLarge' : 16974136, - 'Theme.DeviceDefault.Light.DialogWhenLarge.NoActionBar' : 16974137, - 'Theme.DeviceDefault.Light.Dialog.MinWidth' : 16974131, - 'Theme.DeviceDefault.Light.Dialog.NoActionBar' : 16974132, - 'Theme.DeviceDefault.Light.Dialog.NoActionBar.MinWidth' : 16974133, - 'Theme.DeviceDefault.Light.NoActionBar' : 16974124, - 'Theme.DeviceDefault.Light.NoActionBar.Fullscreen' : 16974125, - 'Theme.DeviceDefault.Light.NoActionBar.Overscan' : 16974304, - 'Theme.DeviceDefault.Light.NoActionBar.TranslucentDecor' : 16974308, - 'Theme.DeviceDefault.Light.Panel' : 16974139, - 'Theme.DeviceDefault.NoActionBar' : 16974121, - 'Theme.DeviceDefault.NoActionBar.Fullscreen' : 16974122, - 'Theme.DeviceDefault.NoActionBar.Overscan' : 16974303, - 'Theme.DeviceDefault.NoActionBar.TranslucentDecor' : 16974307, - 'Theme.DeviceDefault.Panel' : 16974138, - 'Theme.DeviceDefault.Settings' : 16974371, - 'Theme.DeviceDefault.Wallpaper' : 16974140, - 'Theme.DeviceDefault.Wallpaper.NoTitleBar' : 16974141, - 'Theme.Dialog' : 16973835, - 'Theme.Holo' : 16973931, - 'Theme.Holo.Dialog' : 16973935, - 'Theme.Holo.DialogWhenLarge' : 16973943, - 'Theme.Holo.DialogWhenLarge.NoActionBar' : 16973944, - 'Theme.Holo.Dialog.MinWidth' : 16973936, - 'Theme.Holo.Dialog.NoActionBar' : 16973937, - 'Theme.Holo.Dialog.NoActionBar.MinWidth' : 16973938, - 'Theme.Holo.InputMethod' : 16973951, - 'Theme.Holo.Light' : 16973934, - 'Theme.Holo.Light.DarkActionBar' : 16974105, - 'Theme.Holo.Light.Dialog' : 16973939, - 'Theme.Holo.Light.DialogWhenLarge' : 16973945, - 'Theme.Holo.Light.DialogWhenLarge.NoActionBar' : 16973946, - 'Theme.Holo.Light.Dialog.MinWidth' : 16973940, - 'Theme.Holo.Light.Dialog.NoActionBar' : 16973941, - 'Theme.Holo.Light.Dialog.NoActionBar.MinWidth' : 16973942, - 'Theme.Holo.Light.NoActionBar' : 16974064, - 'Theme.Holo.Light.NoActionBar.Fullscreen' : 16974065, - 'Theme.Holo.Light.NoActionBar.Overscan' : 16974302, - 'Theme.Holo.Light.NoActionBar.TranslucentDecor' : 16974306, - 'Theme.Holo.Light.Panel' : 16973948, - 'Theme.Holo.NoActionBar' : 16973932, - 'Theme.Holo.NoActionBar.Fullscreen' : 16973933, - 'Theme.Holo.NoActionBar.Overscan' : 16974301, - 'Theme.Holo.NoActionBar.TranslucentDecor' : 16974305, - 'Theme.Holo.Panel' : 16973947, - 'Theme.Holo.Wallpaper' : 16973949, - 'Theme.Holo.Wallpaper.NoTitleBar' : 16973950, - 'Theme.InputMethod' : 16973908, - 'Theme.Light' : 16973836, - 'Theme.Light.NoTitleBar' : 16973837, - 'Theme.Light.NoTitleBar.Fullscreen' : 16973838, - 'Theme.Light.Panel' : 16973914, - 'Theme.Light.WallpaperSettings' : 16973922, - 'Theme.Material' : 16974372, - 'Theme.Material.Dialog' : 16974373, - 'Theme.Material.DialogWhenLarge' : 16974379, - 'Theme.Material.DialogWhenLarge.NoActionBar' : 16974380, - 'Theme.Material.Dialog.Alert' : 16974374, - 'Theme.Material.Dialog.MinWidth' : 16974375, - 'Theme.Material.Dialog.NoActionBar' : 16974376, - 'Theme.Material.Dialog.NoActionBar.MinWidth' : 16974377, - 'Theme.Material.Dialog.Presentation' : 16974378, - 'Theme.Material.InputMethod' : 16974381, - 'Theme.Material.Light' : 16974391, - 'Theme.Material.Light.DarkActionBar' : 16974392, - 'Theme.Material.Light.Dialog' : 16974393, - 'Theme.Material.Light.DialogWhenLarge' : 16974399, - 'Theme.Material.Light.DialogWhenLarge.NoActionBar' : 16974400, - 'Theme.Material.Light.Dialog.Alert' : 16974394, - 'Theme.Material.Light.Dialog.MinWidth' : 16974395, - 'Theme.Material.Light.Dialog.NoActionBar' : 16974396, - 'Theme.Material.Light.Dialog.NoActionBar.MinWidth' : 16974397, - 'Theme.Material.Light.Dialog.Presentation' : 16974398, - 'Theme.Material.Light.NoActionBar' : 16974401, - 'Theme.Material.Light.NoActionBar.Fullscreen' : 16974402, - 'Theme.Material.Light.NoActionBar.Overscan' : 16974403, - 'Theme.Material.Light.NoActionBar.TranslucentDecor' : 16974404, - 'Theme.Material.Light.Panel' : 16974405, - 'Theme.Material.Light.Voice' : 16974406, - 'Theme.Material.NoActionBar' : 16974382, - 'Theme.Material.NoActionBar.Fullscreen' : 16974383, - 'Theme.Material.NoActionBar.Overscan' : 16974384, - 'Theme.Material.NoActionBar.TranslucentDecor' : 16974385, - 'Theme.Material.Panel' : 16974386, - 'Theme.Material.Settings' : 16974387, - 'Theme.Material.Voice' : 16974388, - 'Theme.Material.Wallpaper' : 16974389, - 'Theme.Material.Wallpaper.NoTitleBar' : 16974390, - 'Theme.NoDisplay' : 16973909, - 'Theme.NoTitleBar' : 16973830, - 'Theme.NoTitleBar.Fullscreen' : 16973831, - 'Theme.NoTitleBar.OverlayActionModes' : 16973930, - 'Theme.Panel' : 16973913, - 'Theme.Translucent' : 16973839, - 'Theme.Translucent.NoTitleBar' : 16973840, - 'Theme.Translucent.NoTitleBar.Fullscreen' : 16973841, - 'Theme.Wallpaper' : 16973918, - 'Theme.WallpaperSettings' : 16973921, - 'Theme.Wallpaper.NoTitleBar' : 16973919, - 'Theme.Wallpaper.NoTitleBar.Fullscreen' : 16973920, - 'Theme.WithActionBar' : 16973929, - 'Widget' : 16973842, - 'Widget.AbsListView' : 16973843, - 'Widget.ActionBar' : 16973954, - 'Widget.ActionBar.TabBar' : 16974068, - 'Widget.ActionBar.TabText' : 16974067, - 'Widget.ActionBar.TabView' : 16974066, - 'Widget.ActionButton' : 16973956, - 'Widget.ActionButton.CloseMode' : 16973960, - 'Widget.ActionButton.Overflow' : 16973959, - 'Widget.AutoCompleteTextView' : 16973863, - 'Widget.Button' : 16973844, - 'Widget.Button.Inset' : 16973845, - 'Widget.Button.Small' : 16973846, - 'Widget.Button.Toggle' : 16973847, - 'Widget.CalendarView' : 16974059, - 'Widget.CompoundButton' : 16973848, - 'Widget.CompoundButton.CheckBox' : 16973849, - 'Widget.CompoundButton.RadioButton' : 16973850, - 'Widget.CompoundButton.Star' : 16973851, - 'Widget.DatePicker' : 16974062, - 'Widget.DeviceDefault' : 16974144, - 'Widget.DeviceDefault.ActionBar' : 16974187, - 'Widget.DeviceDefault.ActionBar.Solid' : 16974195, - 'Widget.DeviceDefault.ActionBar.TabBar' : 16974194, - 'Widget.DeviceDefault.ActionBar.TabText' : 16974193, - 'Widget.DeviceDefault.ActionBar.TabView' : 16974192, - 'Widget.DeviceDefault.ActionButton' : 16974182, - 'Widget.DeviceDefault.ActionButton.CloseMode' : 16974186, - 'Widget.DeviceDefault.ActionButton.Overflow' : 16974183, - 'Widget.DeviceDefault.ActionButton.TextButton' : 16974184, - 'Widget.DeviceDefault.ActionMode' : 16974185, - 'Widget.DeviceDefault.AutoCompleteTextView' : 16974151, - 'Widget.DeviceDefault.Button' : 16974145, - 'Widget.DeviceDefault.Button.Borderless' : 16974188, - 'Widget.DeviceDefault.Button.Borderless.Small' : 16974149, - 'Widget.DeviceDefault.Button.Inset' : 16974147, - 'Widget.DeviceDefault.Button.Small' : 16974146, - 'Widget.DeviceDefault.Button.Toggle' : 16974148, - 'Widget.DeviceDefault.CalendarView' : 16974190, - 'Widget.DeviceDefault.CheckedTextView' : 16974299, - 'Widget.DeviceDefault.CompoundButton.CheckBox' : 16974152, - 'Widget.DeviceDefault.CompoundButton.RadioButton' : 16974169, - 'Widget.DeviceDefault.CompoundButton.Star' : 16974173, - 'Widget.DeviceDefault.DatePicker' : 16974191, - 'Widget.DeviceDefault.DropDownItem' : 16974177, - 'Widget.DeviceDefault.DropDownItem.Spinner' : 16974178, - 'Widget.DeviceDefault.EditText' : 16974154, - 'Widget.DeviceDefault.ExpandableListView' : 16974155, - 'Widget.DeviceDefault.FastScroll' : 16974313, - 'Widget.DeviceDefault.GridView' : 16974156, - 'Widget.DeviceDefault.HorizontalScrollView' : 16974171, - 'Widget.DeviceDefault.ImageButton' : 16974157, - 'Widget.DeviceDefault.Light' : 16974196, - 'Widget.DeviceDefault.Light.ActionBar' : 16974243, - 'Widget.DeviceDefault.Light.ActionBar.Solid' : 16974247, - 'Widget.DeviceDefault.Light.ActionBar.Solid.Inverse' : 16974248, - 'Widget.DeviceDefault.Light.ActionBar.TabBar' : 16974246, - 'Widget.DeviceDefault.Light.ActionBar.TabBar.Inverse' : 16974249, - 'Widget.DeviceDefault.Light.ActionBar.TabText' : 16974245, - 'Widget.DeviceDefault.Light.ActionBar.TabText.Inverse' : 16974251, - 'Widget.DeviceDefault.Light.ActionBar.TabView' : 16974244, - 'Widget.DeviceDefault.Light.ActionBar.TabView.Inverse' : 16974250, - 'Widget.DeviceDefault.Light.ActionButton' : 16974239, - 'Widget.DeviceDefault.Light.ActionButton.CloseMode' : 16974242, - 'Widget.DeviceDefault.Light.ActionButton.Overflow' : 16974240, - 'Widget.DeviceDefault.Light.ActionMode' : 16974241, - 'Widget.DeviceDefault.Light.ActionMode.Inverse' : 16974252, - 'Widget.DeviceDefault.Light.AutoCompleteTextView' : 16974203, - 'Widget.DeviceDefault.Light.Button' : 16974197, - 'Widget.DeviceDefault.Light.Button.Borderless.Small' : 16974201, - 'Widget.DeviceDefault.Light.Button.Inset' : 16974199, - 'Widget.DeviceDefault.Light.Button.Small' : 16974198, - 'Widget.DeviceDefault.Light.Button.Toggle' : 16974200, - 'Widget.DeviceDefault.Light.CalendarView' : 16974238, - 'Widget.DeviceDefault.Light.CheckedTextView' : 16974300, - 'Widget.DeviceDefault.Light.CompoundButton.CheckBox' : 16974204, - 'Widget.DeviceDefault.Light.CompoundButton.RadioButton' : 16974224, - 'Widget.DeviceDefault.Light.CompoundButton.Star' : 16974228, - 'Widget.DeviceDefault.Light.DropDownItem' : 16974232, - 'Widget.DeviceDefault.Light.DropDownItem.Spinner' : 16974233, - 'Widget.DeviceDefault.Light.EditText' : 16974206, - 'Widget.DeviceDefault.Light.ExpandableListView' : 16974207, - 'Widget.DeviceDefault.Light.FastScroll' : 16974315, - 'Widget.DeviceDefault.Light.GridView' : 16974208, - 'Widget.DeviceDefault.Light.HorizontalScrollView' : 16974226, - 'Widget.DeviceDefault.Light.ImageButton' : 16974209, - 'Widget.DeviceDefault.Light.ListPopupWindow' : 16974235, - 'Widget.DeviceDefault.Light.ListView' : 16974210, - 'Widget.DeviceDefault.Light.ListView.DropDown' : 16974205, - 'Widget.DeviceDefault.Light.MediaRouteButton' : 16974296, - 'Widget.DeviceDefault.Light.PopupMenu' : 16974236, - 'Widget.DeviceDefault.Light.PopupWindow' : 16974211, - 'Widget.DeviceDefault.Light.ProgressBar' : 16974212, - 'Widget.DeviceDefault.Light.ProgressBar.Horizontal' : 16974213, - 'Widget.DeviceDefault.Light.ProgressBar.Inverse' : 16974217, - 'Widget.DeviceDefault.Light.ProgressBar.Large' : 16974216, - 'Widget.DeviceDefault.Light.ProgressBar.Large.Inverse' : 16974219, - 'Widget.DeviceDefault.Light.ProgressBar.Small' : 16974214, - 'Widget.DeviceDefault.Light.ProgressBar.Small.Inverse' : 16974218, - 'Widget.DeviceDefault.Light.ProgressBar.Small.Title' : 16974215, - 'Widget.DeviceDefault.Light.RatingBar' : 16974221, - 'Widget.DeviceDefault.Light.RatingBar.Indicator' : 16974222, - 'Widget.DeviceDefault.Light.RatingBar.Small' : 16974223, - 'Widget.DeviceDefault.Light.ScrollView' : 16974225, - 'Widget.DeviceDefault.Light.SeekBar' : 16974220, - 'Widget.DeviceDefault.Light.Spinner' : 16974227, - 'Widget.DeviceDefault.Light.StackView' : 16974316, - 'Widget.DeviceDefault.Light.Tab' : 16974237, - 'Widget.DeviceDefault.Light.TabWidget' : 16974229, - 'Widget.DeviceDefault.Light.TextView' : 16974202, - 'Widget.DeviceDefault.Light.TextView.SpinnerItem' : 16974234, - 'Widget.DeviceDefault.Light.WebTextView' : 16974230, - 'Widget.DeviceDefault.Light.WebView' : 16974231, - 'Widget.DeviceDefault.ListPopupWindow' : 16974180, - 'Widget.DeviceDefault.ListView' : 16974158, - 'Widget.DeviceDefault.ListView.DropDown' : 16974153, - 'Widget.DeviceDefault.MediaRouteButton' : 16974295, - 'Widget.DeviceDefault.PopupMenu' : 16974181, - 'Widget.DeviceDefault.PopupWindow' : 16974159, - 'Widget.DeviceDefault.ProgressBar' : 16974160, - 'Widget.DeviceDefault.ProgressBar.Horizontal' : 16974161, - 'Widget.DeviceDefault.ProgressBar.Large' : 16974164, - 'Widget.DeviceDefault.ProgressBar.Small' : 16974162, - 'Widget.DeviceDefault.ProgressBar.Small.Title' : 16974163, - 'Widget.DeviceDefault.RatingBar' : 16974166, - 'Widget.DeviceDefault.RatingBar.Indicator' : 16974167, - 'Widget.DeviceDefault.RatingBar.Small' : 16974168, - 'Widget.DeviceDefault.ScrollView' : 16974170, - 'Widget.DeviceDefault.SeekBar' : 16974165, - 'Widget.DeviceDefault.Spinner' : 16974172, - 'Widget.DeviceDefault.StackView' : 16974314, - 'Widget.DeviceDefault.Tab' : 16974189, - 'Widget.DeviceDefault.TabWidget' : 16974174, - 'Widget.DeviceDefault.TextView' : 16974150, - 'Widget.DeviceDefault.TextView.SpinnerItem' : 16974179, - 'Widget.DeviceDefault.WebTextView' : 16974175, - 'Widget.DeviceDefault.WebView' : 16974176, - 'Widget.DropDownItem' : 16973867, - 'Widget.DropDownItem.Spinner' : 16973868, - 'Widget.EditText' : 16973859, - 'Widget.ExpandableListView' : 16973860, - 'Widget.FastScroll' : 16974309, - 'Widget.FragmentBreadCrumbs' : 16973961, - 'Widget.Gallery' : 16973877, - 'Widget.GridView' : 16973874, - 'Widget.Holo' : 16973962, - 'Widget.Holo.ActionBar' : 16974004, - 'Widget.Holo.ActionBar.Solid' : 16974113, - 'Widget.Holo.ActionBar.TabBar' : 16974071, - 'Widget.Holo.ActionBar.TabText' : 16974070, - 'Widget.Holo.ActionBar.TabView' : 16974069, - 'Widget.Holo.ActionButton' : 16973999, - 'Widget.Holo.ActionButton.CloseMode' : 16974003, - 'Widget.Holo.ActionButton.Overflow' : 16974000, - 'Widget.Holo.ActionButton.TextButton' : 16974001, - 'Widget.Holo.ActionMode' : 16974002, - 'Widget.Holo.AutoCompleteTextView' : 16973968, - 'Widget.Holo.Button' : 16973963, - 'Widget.Holo.Button.Borderless' : 16974050, - 'Widget.Holo.Button.Borderless.Small' : 16974106, - 'Widget.Holo.Button.Inset' : 16973965, - 'Widget.Holo.Button.Small' : 16973964, - 'Widget.Holo.Button.Toggle' : 16973966, - 'Widget.Holo.CalendarView' : 16974060, - 'Widget.Holo.CheckedTextView' : 16974297, - 'Widget.Holo.CompoundButton.CheckBox' : 16973969, - 'Widget.Holo.CompoundButton.RadioButton' : 16973986, - 'Widget.Holo.CompoundButton.Star' : 16973990, - 'Widget.Holo.DatePicker' : 16974063, - 'Widget.Holo.DropDownItem' : 16973994, - 'Widget.Holo.DropDownItem.Spinner' : 16973995, - 'Widget.Holo.EditText' : 16973971, - 'Widget.Holo.ExpandableListView' : 16973972, - 'Widget.Holo.GridView' : 16973973, - 'Widget.Holo.HorizontalScrollView' : 16973988, - 'Widget.Holo.ImageButton' : 16973974, - 'Widget.Holo.Light' : 16974005, - 'Widget.Holo.Light.ActionBar' : 16974049, - 'Widget.Holo.Light.ActionBar.Solid' : 16974114, - 'Widget.Holo.Light.ActionBar.Solid.Inverse' : 16974115, - 'Widget.Holo.Light.ActionBar.TabBar' : 16974074, - 'Widget.Holo.Light.ActionBar.TabBar.Inverse' : 16974116, - 'Widget.Holo.Light.ActionBar.TabText' : 16974073, - 'Widget.Holo.Light.ActionBar.TabText.Inverse' : 16974118, - 'Widget.Holo.Light.ActionBar.TabView' : 16974072, - 'Widget.Holo.Light.ActionBar.TabView.Inverse' : 16974117, - 'Widget.Holo.Light.ActionButton' : 16974045, - 'Widget.Holo.Light.ActionButton.CloseMode' : 16974048, - 'Widget.Holo.Light.ActionButton.Overflow' : 16974046, - 'Widget.Holo.Light.ActionMode' : 16974047, - 'Widget.Holo.Light.ActionMode.Inverse' : 16974119, - 'Widget.Holo.Light.AutoCompleteTextView' : 16974011, - 'Widget.Holo.Light.Button' : 16974006, - 'Widget.Holo.Light.Button.Borderless.Small' : 16974107, - 'Widget.Holo.Light.Button.Inset' : 16974008, - 'Widget.Holo.Light.Button.Small' : 16974007, - 'Widget.Holo.Light.Button.Toggle' : 16974009, - 'Widget.Holo.Light.CalendarView' : 16974061, - 'Widget.Holo.Light.CheckedTextView' : 16974298, - 'Widget.Holo.Light.CompoundButton.CheckBox' : 16974012, - 'Widget.Holo.Light.CompoundButton.RadioButton' : 16974032, - 'Widget.Holo.Light.CompoundButton.Star' : 16974036, - 'Widget.Holo.Light.DropDownItem' : 16974040, - 'Widget.Holo.Light.DropDownItem.Spinner' : 16974041, - 'Widget.Holo.Light.EditText' : 16974014, - 'Widget.Holo.Light.ExpandableListView' : 16974015, - 'Widget.Holo.Light.GridView' : 16974016, - 'Widget.Holo.Light.HorizontalScrollView' : 16974034, - 'Widget.Holo.Light.ImageButton' : 16974017, - 'Widget.Holo.Light.ListPopupWindow' : 16974043, - 'Widget.Holo.Light.ListView' : 16974018, - 'Widget.Holo.Light.ListView.DropDown' : 16974013, - 'Widget.Holo.Light.MediaRouteButton' : 16974294, - 'Widget.Holo.Light.PopupMenu' : 16974044, - 'Widget.Holo.Light.PopupWindow' : 16974019, - 'Widget.Holo.Light.ProgressBar' : 16974020, - 'Widget.Holo.Light.ProgressBar.Horizontal' : 16974021, - 'Widget.Holo.Light.ProgressBar.Inverse' : 16974025, - 'Widget.Holo.Light.ProgressBar.Large' : 16974024, - 'Widget.Holo.Light.ProgressBar.Large.Inverse' : 16974027, - 'Widget.Holo.Light.ProgressBar.Small' : 16974022, - 'Widget.Holo.Light.ProgressBar.Small.Inverse' : 16974026, - 'Widget.Holo.Light.ProgressBar.Small.Title' : 16974023, - 'Widget.Holo.Light.RatingBar' : 16974029, - 'Widget.Holo.Light.RatingBar.Indicator' : 16974030, - 'Widget.Holo.Light.RatingBar.Small' : 16974031, - 'Widget.Holo.Light.ScrollView' : 16974033, - 'Widget.Holo.Light.SeekBar' : 16974028, - 'Widget.Holo.Light.Spinner' : 16974035, - 'Widget.Holo.Light.Tab' : 16974052, - 'Widget.Holo.Light.TabWidget' : 16974037, - 'Widget.Holo.Light.TextView' : 16974010, - 'Widget.Holo.Light.TextView.SpinnerItem' : 16974042, - 'Widget.Holo.Light.WebTextView' : 16974038, - 'Widget.Holo.Light.WebView' : 16974039, - 'Widget.Holo.ListPopupWindow' : 16973997, - 'Widget.Holo.ListView' : 16973975, - 'Widget.Holo.ListView.DropDown' : 16973970, - 'Widget.Holo.MediaRouteButton' : 16974293, - 'Widget.Holo.PopupMenu' : 16973998, - 'Widget.Holo.PopupWindow' : 16973976, - 'Widget.Holo.ProgressBar' : 16973977, - 'Widget.Holo.ProgressBar.Horizontal' : 16973978, - 'Widget.Holo.ProgressBar.Large' : 16973981, - 'Widget.Holo.ProgressBar.Small' : 16973979, - 'Widget.Holo.ProgressBar.Small.Title' : 16973980, - 'Widget.Holo.RatingBar' : 16973983, - 'Widget.Holo.RatingBar.Indicator' : 16973984, - 'Widget.Holo.RatingBar.Small' : 16973985, - 'Widget.Holo.ScrollView' : 16973987, - 'Widget.Holo.SeekBar' : 16973982, - 'Widget.Holo.Spinner' : 16973989, - 'Widget.Holo.Tab' : 16974051, - 'Widget.Holo.TabWidget' : 16973991, - 'Widget.Holo.TextView' : 16973967, - 'Widget.Holo.TextView.SpinnerItem' : 16973996, - 'Widget.Holo.WebTextView' : 16973992, - 'Widget.Holo.WebView' : 16973993, - 'Widget.ImageButton' : 16973862, - 'Widget.ImageWell' : 16973861, - 'Widget.KeyboardView' : 16973911, - 'Widget.ListPopupWindow' : 16973957, - 'Widget.ListView' : 16973870, - 'Widget.ListView.DropDown' : 16973872, - 'Widget.ListView.Menu' : 16973873, - 'Widget.ListView.White' : 16973871, - 'Widget.Material' : 16974413, - 'Widget.Material.ActionBar' : 16974414, - 'Widget.Material.ActionBar.Solid' : 16974415, - 'Widget.Material.ActionBar.TabBar' : 16974416, - 'Widget.Material.ActionBar.TabText' : 16974417, - 'Widget.Material.ActionBar.TabView' : 16974418, - 'Widget.Material.ActionButton' : 16974419, - 'Widget.Material.ActionButton.CloseMode' : 16974420, - 'Widget.Material.ActionButton.Overflow' : 16974421, - 'Widget.Material.ActionMode' : 16974422, - 'Widget.Material.AutoCompleteTextView' : 16974423, - 'Widget.Material.Button' : 16974424, - 'Widget.Material.ButtonBar' : 16974431, - 'Widget.Material.ButtonBar.AlertDialog' : 16974432, - 'Widget.Material.Button.Borderless' : 16974425, - 'Widget.Material.Button.Borderless.Colored' : 16974426, - 'Widget.Material.Button.Borderless.Small' : 16974427, - 'Widget.Material.Button.Inset' : 16974428, - 'Widget.Material.Button.Small' : 16974429, - 'Widget.Material.Button.Toggle' : 16974430, - 'Widget.Material.CalendarView' : 16974433, - 'Widget.Material.CheckedTextView' : 16974434, - 'Widget.Material.CompoundButton.CheckBox' : 16974435, - 'Widget.Material.CompoundButton.RadioButton' : 16974436, - 'Widget.Material.CompoundButton.Star' : 16974437, - 'Widget.Material.DatePicker' : 16974438, - 'Widget.Material.DropDownItem' : 16974439, - 'Widget.Material.DropDownItem.Spinner' : 16974440, - 'Widget.Material.EditText' : 16974441, - 'Widget.Material.ExpandableListView' : 16974442, - 'Widget.Material.FastScroll' : 16974443, - 'Widget.Material.GridView' : 16974444, - 'Widget.Material.HorizontalScrollView' : 16974445, - 'Widget.Material.ImageButton' : 16974446, - 'Widget.Material.Light' : 16974478, - 'Widget.Material.Light.ActionBar' : 16974479, - 'Widget.Material.Light.ActionBar.Solid' : 16974480, - 'Widget.Material.Light.ActionBar.TabBar' : 16974481, - 'Widget.Material.Light.ActionBar.TabText' : 16974482, - 'Widget.Material.Light.ActionBar.TabView' : 16974483, - 'Widget.Material.Light.ActionButton' : 16974484, - 'Widget.Material.Light.ActionButton.CloseMode' : 16974485, - 'Widget.Material.Light.ActionButton.Overflow' : 16974486, - 'Widget.Material.Light.ActionMode' : 16974487, - 'Widget.Material.Light.AutoCompleteTextView' : 16974488, - 'Widget.Material.Light.Button' : 16974489, - 'Widget.Material.Light.ButtonBar' : 16974496, - 'Widget.Material.Light.ButtonBar.AlertDialog' : 16974497, - 'Widget.Material.Light.Button.Borderless' : 16974490, - 'Widget.Material.Light.Button.Borderless.Colored' : 16974491, - 'Widget.Material.Light.Button.Borderless.Small' : 16974492, - 'Widget.Material.Light.Button.Inset' : 16974493, - 'Widget.Material.Light.Button.Small' : 16974494, - 'Widget.Material.Light.Button.Toggle' : 16974495, - 'Widget.Material.Light.CalendarView' : 16974498, - 'Widget.Material.Light.CheckedTextView' : 16974499, - 'Widget.Material.Light.CompoundButton.CheckBox' : 16974500, - 'Widget.Material.Light.CompoundButton.RadioButton' : 16974501, - 'Widget.Material.Light.CompoundButton.Star' : 16974502, - 'Widget.Material.Light.DatePicker' : 16974503, - 'Widget.Material.Light.DropDownItem' : 16974504, - 'Widget.Material.Light.DropDownItem.Spinner' : 16974505, - 'Widget.Material.Light.EditText' : 16974506, - 'Widget.Material.Light.ExpandableListView' : 16974507, - 'Widget.Material.Light.FastScroll' : 16974508, - 'Widget.Material.Light.GridView' : 16974509, - 'Widget.Material.Light.HorizontalScrollView' : 16974510, - 'Widget.Material.Light.ImageButton' : 16974511, - 'Widget.Material.Light.ListPopupWindow' : 16974512, - 'Widget.Material.Light.ListView' : 16974513, - 'Widget.Material.Light.ListView.DropDown' : 16974514, - 'Widget.Material.Light.MediaRouteButton' : 16974515, - 'Widget.Material.Light.PopupMenu' : 16974516, - 'Widget.Material.Light.PopupMenu.Overflow' : 16974517, - 'Widget.Material.Light.PopupWindow' : 16974518, - 'Widget.Material.Light.ProgressBar' : 16974519, - 'Widget.Material.Light.ProgressBar.Horizontal' : 16974520, - 'Widget.Material.Light.ProgressBar.Inverse' : 16974521, - 'Widget.Material.Light.ProgressBar.Large' : 16974522, - 'Widget.Material.Light.ProgressBar.Large.Inverse' : 16974523, - 'Widget.Material.Light.ProgressBar.Small' : 16974524, - 'Widget.Material.Light.ProgressBar.Small.Inverse' : 16974525, - 'Widget.Material.Light.ProgressBar.Small.Title' : 16974526, - 'Widget.Material.Light.RatingBar' : 16974527, - 'Widget.Material.Light.RatingBar.Indicator' : 16974528, - 'Widget.Material.Light.RatingBar.Small' : 16974529, - 'Widget.Material.Light.ScrollView' : 16974530, - 'Widget.Material.Light.SearchView' : 16974531, - 'Widget.Material.Light.SeekBar' : 16974532, - 'Widget.Material.Light.SegmentedButton' : 16974533, - 'Widget.Material.Light.Spinner' : 16974535, - 'Widget.Material.Light.Spinner.Underlined' : 16974536, - 'Widget.Material.Light.StackView' : 16974534, - 'Widget.Material.Light.Tab' : 16974537, - 'Widget.Material.Light.TabWidget' : 16974538, - 'Widget.Material.Light.TextView' : 16974539, - 'Widget.Material.Light.TextView.SpinnerItem' : 16974540, - 'Widget.Material.Light.TimePicker' : 16974541, - 'Widget.Material.Light.WebTextView' : 16974542, - 'Widget.Material.Light.WebView' : 16974543, - 'Widget.Material.ListPopupWindow' : 16974447, - 'Widget.Material.ListView' : 16974448, - 'Widget.Material.ListView.DropDown' : 16974449, - 'Widget.Material.MediaRouteButton' : 16974450, - 'Widget.Material.PopupMenu' : 16974451, - 'Widget.Material.PopupMenu.Overflow' : 16974452, - 'Widget.Material.PopupWindow' : 16974453, - 'Widget.Material.ProgressBar' : 16974454, - 'Widget.Material.ProgressBar.Horizontal' : 16974455, - 'Widget.Material.ProgressBar.Large' : 16974456, - 'Widget.Material.ProgressBar.Small' : 16974457, - 'Widget.Material.ProgressBar.Small.Title' : 16974458, - 'Widget.Material.RatingBar' : 16974459, - 'Widget.Material.RatingBar.Indicator' : 16974460, - 'Widget.Material.RatingBar.Small' : 16974461, - 'Widget.Material.ScrollView' : 16974462, - 'Widget.Material.SearchView' : 16974463, - 'Widget.Material.SeekBar' : 16974464, - 'Widget.Material.SegmentedButton' : 16974465, - 'Widget.Material.Spinner' : 16974467, - 'Widget.Material.Spinner.Underlined' : 16974468, - 'Widget.Material.StackView' : 16974466, - 'Widget.Material.Tab' : 16974469, - 'Widget.Material.TabWidget' : 16974470, - 'Widget.Material.TextView' : 16974471, - 'Widget.Material.TextView.SpinnerItem' : 16974472, - 'Widget.Material.TimePicker' : 16974473, - 'Widget.Material.Toolbar' : 16974474, - 'Widget.Material.Toolbar.Button.Navigation' : 16974475, - 'Widget.Material.WebTextView' : 16974476, - 'Widget.Material.WebView' : 16974477, - 'Widget.PopupMenu' : 16973958, - 'Widget.PopupWindow' : 16973878, - 'Widget.ProgressBar' : 16973852, - 'Widget.ProgressBar.Horizontal' : 16973855, - 'Widget.ProgressBar.Inverse' : 16973915, - 'Widget.ProgressBar.Large' : 16973853, - 'Widget.ProgressBar.Large.Inverse' : 16973916, - 'Widget.ProgressBar.Small' : 16973854, - 'Widget.ProgressBar.Small.Inverse' : 16973917, - 'Widget.RatingBar' : 16973857, - 'Widget.ScrollView' : 16973869, - 'Widget.SeekBar' : 16973856, - 'Widget.Spinner' : 16973864, - 'Widget.Spinner.DropDown' : 16973955, - 'Widget.StackView' : 16974310, - 'Widget.TabWidget' : 16973876, - 'Widget.TextView' : 16973858, - 'Widget.TextView.PopupMenu' : 16973865, - 'Widget.TextView.SpinnerItem' : 16973866, - 'Widget.Toolbar' : 16974311, - 'Widget.Toolbar.Button.Navigation' : 16974312, - 'Widget.WebView' : 16973875, -}, - 'attr': { - 'theme' : 16842752, - 'label' : 16842753, - 'icon' : 16842754, - 'name' : 16842755, - 'manageSpaceActivity' : 16842756, - 'allowClearUserData' : 16842757, - 'permission' : 16842758, - 'readPermission' : 16842759, - 'writePermission' : 16842760, - 'protectionLevel' : 16842761, - 'permissionGroup' : 16842762, - 'sharedUserId' : 16842763, - 'hasCode' : 16842764, - 'persistent' : 16842765, - 'enabled' : 16842766, - 'debuggable' : 16842767, - 'exported' : 16842768, - 'process' : 16842769, - 'taskAffinity' : 16842770, - 'multiprocess' : 16842771, - 'finishOnTaskLaunch' : 16842772, - 'clearTaskOnLaunch' : 16842773, - 'stateNotNeeded' : 16842774, - 'excludeFromRecents' : 16842775, - 'authorities' : 16842776, - 'syncable' : 16842777, - 'initOrder' : 16842778, - 'grantUriPermissions' : 16842779, - 'priority' : 16842780, - 'launchMode' : 16842781, - 'screenOrientation' : 16842782, - 'configChanges' : 16842783, - 'description' : 16842784, - 'targetPackage' : 16842785, - 'handleProfiling' : 16842786, - 'functionalTest' : 16842787, - 'value' : 16842788, - 'resource' : 16842789, - 'mimeType' : 16842790, - 'scheme' : 16842791, - 'host' : 16842792, - 'port' : 16842793, - 'path' : 16842794, - 'pathPrefix' : 16842795, - 'pathPattern' : 16842796, - 'action' : 16842797, - 'data' : 16842798, - 'targetClass' : 16842799, - 'colorForeground' : 16842800, - 'colorBackground' : 16842801, - 'backgroundDimAmount' : 16842802, - 'disabledAlpha' : 16842803, - 'textAppearance' : 16842804, - 'textAppearanceInverse' : 16842805, - 'textColorPrimary' : 16842806, - 'textColorPrimaryDisableOnly' : 16842807, - 'textColorSecondary' : 16842808, - 'textColorPrimaryInverse' : 16842809, - 'textColorSecondaryInverse' : 16842810, - 'textColorPrimaryNoDisable' : 16842811, - 'textColorSecondaryNoDisable' : 16842812, - 'textColorPrimaryInverseNoDisable' : 16842813, - 'textColorSecondaryInverseNoDisable' : 16842814, - 'textColorHintInverse' : 16842815, - 'textAppearanceLarge' : 16842816, - 'textAppearanceMedium' : 16842817, - 'textAppearanceSmall' : 16842818, - 'textAppearanceLargeInverse' : 16842819, - 'textAppearanceMediumInverse' : 16842820, - 'textAppearanceSmallInverse' : 16842821, - 'textCheckMark' : 16842822, - 'textCheckMarkInverse' : 16842823, - 'buttonStyle' : 16842824, - 'buttonStyleSmall' : 16842825, - 'buttonStyleInset' : 16842826, - 'buttonStyleToggle' : 16842827, - 'galleryItemBackground' : 16842828, - 'listPreferredItemHeight' : 16842829, - 'expandableListPreferredItemPaddingLeft' : 16842830, - 'expandableListPreferredChildPaddingLeft' : 16842831, - 'expandableListPreferredItemIndicatorLeft' : 16842832, - 'expandableListPreferredItemIndicatorRight' : 16842833, - 'expandableListPreferredChildIndicatorLeft' : 16842834, - 'expandableListPreferredChildIndicatorRight' : 16842835, - 'windowBackground' : 16842836, - 'windowFrame' : 16842837, - 'windowNoTitle' : 16842838, - 'windowIsFloating' : 16842839, - 'windowIsTranslucent' : 16842840, - 'windowContentOverlay' : 16842841, - 'windowTitleSize' : 16842842, - 'windowTitleStyle' : 16842843, - 'windowTitleBackgroundStyle' : 16842844, - 'alertDialogStyle' : 16842845, - 'panelBackground' : 16842846, - 'panelFullBackground' : 16842847, - 'panelColorForeground' : 16842848, - 'panelColorBackground' : 16842849, - 'panelTextAppearance' : 16842850, - 'scrollbarSize' : 16842851, - 'scrollbarThumbHorizontal' : 16842852, - 'scrollbarThumbVertical' : 16842853, - 'scrollbarTrackHorizontal' : 16842854, - 'scrollbarTrackVertical' : 16842855, - 'scrollbarAlwaysDrawHorizontalTrack' : 16842856, - 'scrollbarAlwaysDrawVerticalTrack' : 16842857, - 'absListViewStyle' : 16842858, - 'autoCompleteTextViewStyle' : 16842859, - 'checkboxStyle' : 16842860, - 'dropDownListViewStyle' : 16842861, - 'editTextStyle' : 16842862, - 'expandableListViewStyle' : 16842863, - 'galleryStyle' : 16842864, - 'gridViewStyle' : 16842865, - 'imageButtonStyle' : 16842866, - 'imageWellStyle' : 16842867, - 'listViewStyle' : 16842868, - 'listViewWhiteStyle' : 16842869, - 'popupWindowStyle' : 16842870, - 'progressBarStyle' : 16842871, - 'progressBarStyleHorizontal' : 16842872, - 'progressBarStyleSmall' : 16842873, - 'progressBarStyleLarge' : 16842874, - 'seekBarStyle' : 16842875, - 'ratingBarStyle' : 16842876, - 'ratingBarStyleSmall' : 16842877, - 'radioButtonStyle' : 16842878, - 'scrollbarStyle' : 16842879, - 'scrollViewStyle' : 16842880, - 'spinnerStyle' : 16842881, - 'starStyle' : 16842882, - 'tabWidgetStyle' : 16842883, - 'textViewStyle' : 16842884, - 'webViewStyle' : 16842885, - 'dropDownItemStyle' : 16842886, - 'spinnerDropDownItemStyle' : 16842887, - 'dropDownHintAppearance' : 16842888, - 'spinnerItemStyle' : 16842889, - 'mapViewStyle' : 16842890, - 'preferenceScreenStyle' : 16842891, - 'preferenceCategoryStyle' : 16842892, - 'preferenceInformationStyle' : 16842893, - 'preferenceStyle' : 16842894, - 'checkBoxPreferenceStyle' : 16842895, - 'yesNoPreferenceStyle' : 16842896, - 'dialogPreferenceStyle' : 16842897, - 'editTextPreferenceStyle' : 16842898, - 'ringtonePreferenceStyle' : 16842899, - 'preferenceLayoutChild' : 16842900, - 'textSize' : 16842901, - 'typeface' : 16842902, - 'textStyle' : 16842903, - 'textColor' : 16842904, - 'textColorHighlight' : 16842905, - 'textColorHint' : 16842906, - 'textColorLink' : 16842907, - 'state_focused' : 16842908, - 'state_window_focused' : 16842909, - 'state_enabled' : 16842910, - 'state_checkable' : 16842911, - 'state_checked' : 16842912, - 'state_selected' : 16842913, - 'state_active' : 16842914, - 'state_single' : 16842915, - 'state_first' : 16842916, - 'state_middle' : 16842917, - 'state_last' : 16842918, - 'state_pressed' : 16842919, - 'state_expanded' : 16842920, - 'state_empty' : 16842921, - 'state_above_anchor' : 16842922, - 'ellipsize' : 16842923, - 'x' : 16842924, - 'y' : 16842925, - 'windowAnimationStyle' : 16842926, - 'gravity' : 16842927, - 'autoLink' : 16842928, - 'linksClickable' : 16842929, - 'entries' : 16842930, - 'layout_gravity' : 16842931, - 'windowEnterAnimation' : 16842932, - 'windowExitAnimation' : 16842933, - 'windowShowAnimation' : 16842934, - 'windowHideAnimation' : 16842935, - 'activityOpenEnterAnimation' : 16842936, - 'activityOpenExitAnimation' : 16842937, - 'activityCloseEnterAnimation' : 16842938, - 'activityCloseExitAnimation' : 16842939, - 'taskOpenEnterAnimation' : 16842940, - 'taskOpenExitAnimation' : 16842941, - 'taskCloseEnterAnimation' : 16842942, - 'taskCloseExitAnimation' : 16842943, - 'taskToFrontEnterAnimation' : 16842944, - 'taskToFrontExitAnimation' : 16842945, - 'taskToBackEnterAnimation' : 16842946, - 'taskToBackExitAnimation' : 16842947, - 'orientation' : 16842948, - 'keycode' : 16842949, - 'fullDark' : 16842950, - 'topDark' : 16842951, - 'centerDark' : 16842952, - 'bottomDark' : 16842953, - 'fullBright' : 16842954, - 'topBright' : 16842955, - 'centerBright' : 16842956, - 'bottomBright' : 16842957, - 'bottomMedium' : 16842958, - 'centerMedium' : 16842959, - 'id' : 16842960, - 'tag' : 16842961, - 'scrollX' : 16842962, - 'scrollY' : 16842963, - 'background' : 16842964, - 'padding' : 16842965, - 'paddingLeft' : 16842966, - 'paddingTop' : 16842967, - 'paddingRight' : 16842968, - 'paddingBottom' : 16842969, - 'focusable' : 16842970, - 'focusableInTouchMode' : 16842971, - 'visibility' : 16842972, - 'fitsSystemWindows' : 16842973, - 'scrollbars' : 16842974, - 'fadingEdge' : 16842975, - 'fadingEdgeLength' : 16842976, - 'nextFocusLeft' : 16842977, - 'nextFocusRight' : 16842978, - 'nextFocusUp' : 16842979, - 'nextFocusDown' : 16842980, - 'clickable' : 16842981, - 'longClickable' : 16842982, - 'saveEnabled' : 16842983, - 'drawingCacheQuality' : 16842984, - 'duplicateParentState' : 16842985, - 'clipChildren' : 16842986, - 'clipToPadding' : 16842987, - 'layoutAnimation' : 16842988, - 'animationCache' : 16842989, - 'persistentDrawingCache' : 16842990, - 'alwaysDrawnWithCache' : 16842991, - 'addStatesFromChildren' : 16842992, - 'descendantFocusability' : 16842993, - 'layout' : 16842994, - 'inflatedId' : 16842995, - 'layout_width' : 16842996, - 'layout_height' : 16842997, - 'layout_margin' : 16842998, - 'layout_marginLeft' : 16842999, - 'layout_marginTop' : 16843000, - 'layout_marginRight' : 16843001, - 'layout_marginBottom' : 16843002, - 'listSelector' : 16843003, - 'drawSelectorOnTop' : 16843004, - 'stackFromBottom' : 16843005, - 'scrollingCache' : 16843006, - 'textFilterEnabled' : 16843007, - 'transcriptMode' : 16843008, - 'cacheColorHint' : 16843009, - 'dial' : 16843010, - 'hand_hour' : 16843011, - 'hand_minute' : 16843012, - 'format' : 16843013, - 'checked' : 16843014, - 'button' : 16843015, - 'checkMark' : 16843016, - 'foreground' : 16843017, - 'measureAllChildren' : 16843018, - 'groupIndicator' : 16843019, - 'childIndicator' : 16843020, - 'indicatorLeft' : 16843021, - 'indicatorRight' : 16843022, - 'childIndicatorLeft' : 16843023, - 'childIndicatorRight' : 16843024, - 'childDivider' : 16843025, - 'animationDuration' : 16843026, - 'spacing' : 16843027, - 'horizontalSpacing' : 16843028, - 'verticalSpacing' : 16843029, - 'stretchMode' : 16843030, - 'columnWidth' : 16843031, - 'numColumns' : 16843032, - 'src' : 16843033, - 'antialias' : 16843034, - 'filter' : 16843035, - 'dither' : 16843036, - 'scaleType' : 16843037, - 'adjustViewBounds' : 16843038, - 'maxWidth' : 16843039, - 'maxHeight' : 16843040, - 'tint' : 16843041, - 'baselineAlignBottom' : 16843042, - 'cropToPadding' : 16843043, - 'textOn' : 16843044, - 'textOff' : 16843045, - 'baselineAligned' : 16843046, - 'baselineAlignedChildIndex' : 16843047, - 'weightSum' : 16843048, - 'divider' : 16843049, - 'dividerHeight' : 16843050, - 'choiceMode' : 16843051, - 'itemTextAppearance' : 16843052, - 'horizontalDivider' : 16843053, - 'verticalDivider' : 16843054, - 'headerBackground' : 16843055, - 'itemBackground' : 16843056, - 'itemIconDisabledAlpha' : 16843057, - 'rowHeight' : 16843058, - 'maxRows' : 16843059, - 'maxItemsPerRow' : 16843060, - 'moreIcon' : 16843061, - 'max' : 16843062, - 'progress' : 16843063, - 'secondaryProgress' : 16843064, - 'indeterminate' : 16843065, - 'indeterminateOnly' : 16843066, - 'indeterminateDrawable' : 16843067, - 'progressDrawable' : 16843068, - 'indeterminateDuration' : 16843069, - 'indeterminateBehavior' : 16843070, - 'minWidth' : 16843071, - 'minHeight' : 16843072, - 'interpolator' : 16843073, - 'thumb' : 16843074, - 'thumbOffset' : 16843075, - 'numStars' : 16843076, - 'rating' : 16843077, - 'stepSize' : 16843078, - 'isIndicator' : 16843079, - 'checkedButton' : 16843080, - 'stretchColumns' : 16843081, - 'shrinkColumns' : 16843082, - 'collapseColumns' : 16843083, - 'layout_column' : 16843084, - 'layout_span' : 16843085, - 'bufferType' : 16843086, - 'text' : 16843087, - 'hint' : 16843088, - 'textScaleX' : 16843089, - 'cursorVisible' : 16843090, - 'maxLines' : 16843091, - 'lines' : 16843092, - 'height' : 16843093, - 'minLines' : 16843094, - 'maxEms' : 16843095, - 'ems' : 16843096, - 'width' : 16843097, - 'minEms' : 16843098, - 'scrollHorizontally' : 16843099, - 'password' : 16843100, - 'singleLine' : 16843101, - 'selectAllOnFocus' : 16843102, - 'includeFontPadding' : 16843103, - 'maxLength' : 16843104, - 'shadowColor' : 16843105, - 'shadowDx' : 16843106, - 'shadowDy' : 16843107, - 'shadowRadius' : 16843108, - 'numeric' : 16843109, - 'digits' : 16843110, - 'phoneNumber' : 16843111, - 'inputMethod' : 16843112, - 'capitalize' : 16843113, - 'autoText' : 16843114, - 'editable' : 16843115, - 'freezesText' : 16843116, - 'drawableTop' : 16843117, - 'drawableBottom' : 16843118, - 'drawableLeft' : 16843119, - 'drawableRight' : 16843120, - 'drawablePadding' : 16843121, - 'completionHint' : 16843122, - 'completionHintView' : 16843123, - 'completionThreshold' : 16843124, - 'dropDownSelector' : 16843125, - 'popupBackground' : 16843126, - 'inAnimation' : 16843127, - 'outAnimation' : 16843128, - 'flipInterval' : 16843129, - 'fillViewport' : 16843130, - 'prompt' : 16843131, - 'startYear' : 16843132, - 'endYear' : 16843133, - 'mode' : 16843134, - 'layout_x' : 16843135, - 'layout_y' : 16843136, - 'layout_weight' : 16843137, - 'layout_toLeftOf' : 16843138, - 'layout_toRightOf' : 16843139, - 'layout_above' : 16843140, - 'layout_below' : 16843141, - 'layout_alignBaseline' : 16843142, - 'layout_alignLeft' : 16843143, - 'layout_alignTop' : 16843144, - 'layout_alignRight' : 16843145, - 'layout_alignBottom' : 16843146, - 'layout_alignParentLeft' : 16843147, - 'layout_alignParentTop' : 16843148, - 'layout_alignParentRight' : 16843149, - 'layout_alignParentBottom' : 16843150, - 'layout_centerInParent' : 16843151, - 'layout_centerHorizontal' : 16843152, - 'layout_centerVertical' : 16843153, - 'layout_alignWithParentIfMissing' : 16843154, - 'layout_scale' : 16843155, - 'visible' : 16843156, - 'variablePadding' : 16843157, - 'constantSize' : 16843158, - 'oneshot' : 16843159, - 'duration' : 16843160, - 'drawable' : 16843161, - 'shape' : 16843162, - 'innerRadiusRatio' : 16843163, - 'thicknessRatio' : 16843164, - 'startColor' : 16843165, - 'endColor' : 16843166, - 'useLevel' : 16843167, - 'angle' : 16843168, - 'type' : 16843169, - 'centerX' : 16843170, - 'centerY' : 16843171, - 'gradientRadius' : 16843172, - 'color' : 16843173, - 'dashWidth' : 16843174, - 'dashGap' : 16843175, - 'radius' : 16843176, - 'topLeftRadius' : 16843177, - 'topRightRadius' : 16843178, - 'bottomLeftRadius' : 16843179, - 'bottomRightRadius' : 16843180, - 'left' : 16843181, - 'top' : 16843182, - 'right' : 16843183, - 'bottom' : 16843184, - 'minLevel' : 16843185, - 'maxLevel' : 16843186, - 'fromDegrees' : 16843187, - 'toDegrees' : 16843188, - 'pivotX' : 16843189, - 'pivotY' : 16843190, - 'insetLeft' : 16843191, - 'insetRight' : 16843192, - 'insetTop' : 16843193, - 'insetBottom' : 16843194, - 'shareInterpolator' : 16843195, - 'fillBefore' : 16843196, - 'fillAfter' : 16843197, - 'startOffset' : 16843198, - 'repeatCount' : 16843199, - 'repeatMode' : 16843200, - 'zAdjustment' : 16843201, - 'fromXScale' : 16843202, - 'toXScale' : 16843203, - 'fromYScale' : 16843204, - 'toYScale' : 16843205, - 'fromXDelta' : 16843206, - 'toXDelta' : 16843207, - 'fromYDelta' : 16843208, - 'toYDelta' : 16843209, - 'fromAlpha' : 16843210, - 'toAlpha' : 16843211, - 'delay' : 16843212, - 'animation' : 16843213, - 'animationOrder' : 16843214, - 'columnDelay' : 16843215, - 'rowDelay' : 16843216, - 'direction' : 16843217, - 'directionPriority' : 16843218, - 'factor' : 16843219, - 'cycles' : 16843220, - 'searchMode' : 16843221, - 'searchSuggestAuthority' : 16843222, - 'searchSuggestPath' : 16843223, - 'searchSuggestSelection' : 16843224, - 'searchSuggestIntentAction' : 16843225, - 'searchSuggestIntentData' : 16843226, - 'queryActionMsg' : 16843227, - 'suggestActionMsg' : 16843228, - 'suggestActionMsgColumn' : 16843229, - 'menuCategory' : 16843230, - 'orderInCategory' : 16843231, - 'checkableBehavior' : 16843232, - 'title' : 16843233, - 'titleCondensed' : 16843234, - 'alphabeticShortcut' : 16843235, - 'numericShortcut' : 16843236, - 'checkable' : 16843237, - 'selectable' : 16843238, - 'orderingFromXml' : 16843239, - 'key' : 16843240, - 'summary' : 16843241, - 'order' : 16843242, - 'widgetLayout' : 16843243, - 'dependency' : 16843244, - 'defaultValue' : 16843245, - 'shouldDisableView' : 16843246, - 'summaryOn' : 16843247, - 'summaryOff' : 16843248, - 'disableDependentsState' : 16843249, - 'dialogTitle' : 16843250, - 'dialogMessage' : 16843251, - 'dialogIcon' : 16843252, - 'positiveButtonText' : 16843253, - 'negativeButtonText' : 16843254, - 'dialogLayout' : 16843255, - 'entryValues' : 16843256, - 'ringtoneType' : 16843257, - 'showDefault' : 16843258, - 'showSilent' : 16843259, - 'scaleWidth' : 16843260, - 'scaleHeight' : 16843261, - 'scaleGravity' : 16843262, - 'ignoreGravity' : 16843263, - 'foregroundGravity' : 16843264, - 'tileMode' : 16843265, - 'targetActivity' : 16843266, - 'alwaysRetainTaskState' : 16843267, - 'allowTaskReparenting' : 16843268, - 'searchButtonText' : 16843269, - 'colorForegroundInverse' : 16843270, - 'textAppearanceButton' : 16843271, - 'listSeparatorTextViewStyle' : 16843272, - 'streamType' : 16843273, - 'clipOrientation' : 16843274, - 'centerColor' : 16843275, - 'minSdkVersion' : 16843276, - 'windowFullscreen' : 16843277, - 'unselectedAlpha' : 16843278, - 'progressBarStyleSmallTitle' : 16843279, - 'ratingBarStyleIndicator' : 16843280, - 'apiKey' : 16843281, - 'textColorTertiary' : 16843282, - 'textColorTertiaryInverse' : 16843283, - 'listDivider' : 16843284, - 'soundEffectsEnabled' : 16843285, - 'keepScreenOn' : 16843286, - 'lineSpacingExtra' : 16843287, - 'lineSpacingMultiplier' : 16843288, - 'listChoiceIndicatorSingle' : 16843289, - 'listChoiceIndicatorMultiple' : 16843290, - 'versionCode' : 16843291, - 'versionName' : 16843292, - 'marqueeRepeatLimit' : 16843293, - 'windowNoDisplay' : 16843294, - 'backgroundDimEnabled' : 16843295, - 'inputType' : 16843296, - 'isDefault' : 16843297, - 'windowDisablePreview' : 16843298, - 'privateImeOptions' : 16843299, - 'editorExtras' : 16843300, - 'settingsActivity' : 16843301, - 'fastScrollEnabled' : 16843302, - 'reqTouchScreen' : 16843303, - 'reqKeyboardType' : 16843304, - 'reqHardKeyboard' : 16843305, - 'reqNavigation' : 16843306, - 'windowSoftInputMode' : 16843307, - 'imeFullscreenBackground' : 16843308, - 'noHistory' : 16843309, - 'headerDividersEnabled' : 16843310, - 'footerDividersEnabled' : 16843311, - 'candidatesTextStyleSpans' : 16843312, - 'smoothScrollbar' : 16843313, - 'reqFiveWayNav' : 16843314, - 'keyBackground' : 16843315, - 'keyTextSize' : 16843316, - 'labelTextSize' : 16843317, - 'keyTextColor' : 16843318, - 'keyPreviewLayout' : 16843319, - 'keyPreviewOffset' : 16843320, - 'keyPreviewHeight' : 16843321, - 'verticalCorrection' : 16843322, - 'popupLayout' : 16843323, - 'state_long_pressable' : 16843324, - 'keyWidth' : 16843325, - 'keyHeight' : 16843326, - 'horizontalGap' : 16843327, - 'verticalGap' : 16843328, - 'rowEdgeFlags' : 16843329, - 'codes' : 16843330, - 'popupKeyboard' : 16843331, - 'popupCharacters' : 16843332, - 'keyEdgeFlags' : 16843333, - 'isModifier' : 16843334, - 'isSticky' : 16843335, - 'isRepeatable' : 16843336, - 'iconPreview' : 16843337, - 'keyOutputText' : 16843338, - 'keyLabel' : 16843339, - 'keyIcon' : 16843340, - 'keyboardMode' : 16843341, - 'isScrollContainer' : 16843342, - 'fillEnabled' : 16843343, - 'updatePeriodMillis' : 16843344, - 'initialLayout' : 16843345, - 'voiceSearchMode' : 16843346, - 'voiceLanguageModel' : 16843347, - 'voicePromptText' : 16843348, - 'voiceLanguage' : 16843349, - 'voiceMaxResults' : 16843350, - 'bottomOffset' : 16843351, - 'topOffset' : 16843352, - 'allowSingleTap' : 16843353, - 'handle' : 16843354, - 'content' : 16843355, - 'animateOnClick' : 16843356, - 'configure' : 16843357, - 'hapticFeedbackEnabled' : 16843358, - 'innerRadius' : 16843359, - 'thickness' : 16843360, - 'sharedUserLabel' : 16843361, - 'dropDownWidth' : 16843362, - 'dropDownAnchor' : 16843363, - 'imeOptions' : 16843364, - 'imeActionLabel' : 16843365, - 'imeActionId' : 16843366, - 'imeExtractEnterAnimation' : 16843368, - 'imeExtractExitAnimation' : 16843369, - 'tension' : 16843370, - 'extraTension' : 16843371, - 'anyDensity' : 16843372, - 'searchSuggestThreshold' : 16843373, - 'includeInGlobalSearch' : 16843374, - 'onClick' : 16843375, - 'targetSdkVersion' : 16843376, - 'maxSdkVersion' : 16843377, - 'testOnly' : 16843378, - 'contentDescription' : 16843379, - 'gestureStrokeWidth' : 16843380, - 'gestureColor' : 16843381, - 'uncertainGestureColor' : 16843382, - 'fadeOffset' : 16843383, - 'fadeDuration' : 16843384, - 'gestureStrokeType' : 16843385, - 'gestureStrokeLengthThreshold' : 16843386, - 'gestureStrokeSquarenessThreshold' : 16843387, - 'gestureStrokeAngleThreshold' : 16843388, - 'eventsInterceptionEnabled' : 16843389, - 'fadeEnabled' : 16843390, - 'backupAgent' : 16843391, - 'allowBackup' : 16843392, - 'glEsVersion' : 16843393, - 'queryAfterZeroResults' : 16843394, - 'dropDownHeight' : 16843395, - 'smallScreens' : 16843396, - 'normalScreens' : 16843397, - 'largeScreens' : 16843398, - 'progressBarStyleInverse' : 16843399, - 'progressBarStyleSmallInverse' : 16843400, - 'progressBarStyleLargeInverse' : 16843401, - 'searchSettingsDescription' : 16843402, - 'textColorPrimaryInverseDisableOnly' : 16843403, - 'autoUrlDetect' : 16843404, - 'resizeable' : 16843405, - 'required' : 16843406, - 'accountType' : 16843407, - 'contentAuthority' : 16843408, - 'userVisible' : 16843409, - 'windowShowWallpaper' : 16843410, - 'wallpaperOpenEnterAnimation' : 16843411, - 'wallpaperOpenExitAnimation' : 16843412, - 'wallpaperCloseEnterAnimation' : 16843413, - 'wallpaperCloseExitAnimation' : 16843414, - 'wallpaperIntraOpenEnterAnimation' : 16843415, - 'wallpaperIntraOpenExitAnimation' : 16843416, - 'wallpaperIntraCloseEnterAnimation' : 16843417, - 'wallpaperIntraCloseExitAnimation' : 16843418, - 'supportsUploading' : 16843419, - 'killAfterRestore' : 16843420, - 'restoreNeedsApplication' : 16843421, - 'smallIcon' : 16843422, - 'accountPreferences' : 16843423, - 'textAppearanceSearchResultSubtitle' : 16843424, - 'textAppearanceSearchResultTitle' : 16843425, - 'summaryColumn' : 16843426, - 'detailColumn' : 16843427, - 'detailSocialSummary' : 16843428, - 'thumbnail' : 16843429, - 'detachWallpaper' : 16843430, - 'finishOnCloseSystemDialogs' : 16843431, - 'scrollbarFadeDuration' : 16843432, - 'scrollbarDefaultDelayBeforeFade' : 16843433, - 'fadeScrollbars' : 16843434, - 'colorBackgroundCacheHint' : 16843435, - 'dropDownHorizontalOffset' : 16843436, - 'dropDownVerticalOffset' : 16843437, - 'quickContactBadgeStyleWindowSmall' : 16843438, - 'quickContactBadgeStyleWindowMedium' : 16843439, - 'quickContactBadgeStyleWindowLarge' : 16843440, - 'quickContactBadgeStyleSmallWindowSmall' : 16843441, - 'quickContactBadgeStyleSmallWindowMedium' : 16843442, - 'quickContactBadgeStyleSmallWindowLarge' : 16843443, - 'author' : 16843444, - 'autoStart' : 16843445, - 'expandableListViewWhiteStyle' : 16843446, - 'installLocation' : 16843447, - 'vmSafeMode' : 16843448, - 'webTextViewStyle' : 16843449, - 'restoreAnyVersion' : 16843450, - 'tabStripLeft' : 16843451, - 'tabStripRight' : 16843452, - 'tabStripEnabled' : 16843453, - 'logo' : 16843454, - 'xlargeScreens' : 16843455, - 'immersive' : 16843456, - 'overScrollMode' : 16843457, - 'overScrollHeader' : 16843458, - 'overScrollFooter' : 16843459, - 'filterTouchesWhenObscured' : 16843460, - 'textSelectHandleLeft' : 16843461, - 'textSelectHandleRight' : 16843462, - 'textSelectHandle' : 16843463, - 'textSelectHandleWindowStyle' : 16843464, - 'popupAnimationStyle' : 16843465, - 'screenSize' : 16843466, - 'screenDensity' : 16843467, - 'allContactsName' : 16843468, - 'windowActionBar' : 16843469, - 'actionBarStyle' : 16843470, - 'navigationMode' : 16843471, - 'displayOptions' : 16843472, - 'subtitle' : 16843473, - 'customNavigationLayout' : 16843474, - 'hardwareAccelerated' : 16843475, - 'measureWithLargestChild' : 16843476, - 'animateFirstView' : 16843477, - 'dropDownSpinnerStyle' : 16843478, - 'actionDropDownStyle' : 16843479, - 'actionButtonStyle' : 16843480, - 'showAsAction' : 16843481, - 'previewImage' : 16843482, - 'actionModeBackground' : 16843483, - 'actionModeCloseDrawable' : 16843484, - 'windowActionModeOverlay' : 16843485, - 'valueFrom' : 16843486, - 'valueTo' : 16843487, - 'valueType' : 16843488, - 'propertyName' : 16843489, - 'ordering' : 16843490, - 'fragment' : 16843491, - 'windowActionBarOverlay' : 16843492, - 'fragmentOpenEnterAnimation' : 16843493, - 'fragmentOpenExitAnimation' : 16843494, - 'fragmentCloseEnterAnimation' : 16843495, - 'fragmentCloseExitAnimation' : 16843496, - 'fragmentFadeEnterAnimation' : 16843497, - 'fragmentFadeExitAnimation' : 16843498, - 'actionBarSize' : 16843499, - 'imeSubtypeLocale' : 16843500, - 'imeSubtypeMode' : 16843501, - 'imeSubtypeExtraValue' : 16843502, - 'splitMotionEvents' : 16843503, - 'listChoiceBackgroundIndicator' : 16843504, - 'spinnerMode' : 16843505, - 'animateLayoutChanges' : 16843506, - 'actionBarTabStyle' : 16843507, - 'actionBarTabBarStyle' : 16843508, - 'actionBarTabTextStyle' : 16843509, - 'actionOverflowButtonStyle' : 16843510, - 'actionModeCloseButtonStyle' : 16843511, - 'titleTextStyle' : 16843512, - 'subtitleTextStyle' : 16843513, - 'iconifiedByDefault' : 16843514, - 'actionLayout' : 16843515, - 'actionViewClass' : 16843516, - 'activatedBackgroundIndicator' : 16843517, - 'state_activated' : 16843518, - 'listPopupWindowStyle' : 16843519, - 'popupMenuStyle' : 16843520, - 'textAppearanceLargePopupMenu' : 16843521, - 'textAppearanceSmallPopupMenu' : 16843522, - 'breadCrumbTitle' : 16843523, - 'breadCrumbShortTitle' : 16843524, - 'listDividerAlertDialog' : 16843525, - 'textColorAlertDialogListItem' : 16843526, - 'loopViews' : 16843527, - 'dialogTheme' : 16843528, - 'alertDialogTheme' : 16843529, - 'dividerVertical' : 16843530, - 'homeAsUpIndicator' : 16843531, - 'enterFadeDuration' : 16843532, - 'exitFadeDuration' : 16843533, - 'selectableItemBackground' : 16843534, - 'autoAdvanceViewId' : 16843535, - 'useIntrinsicSizeAsMinimum' : 16843536, - 'actionModeCutDrawable' : 16843537, - 'actionModeCopyDrawable' : 16843538, - 'actionModePasteDrawable' : 16843539, - 'textEditPasteWindowLayout' : 16843540, - 'textEditNoPasteWindowLayout' : 16843541, - 'textIsSelectable' : 16843542, - 'windowEnableSplitTouch' : 16843543, - 'indeterminateProgressStyle' : 16843544, - 'progressBarPadding' : 16843545, - 'animationResolution' : 16843546, - 'state_accelerated' : 16843547, - 'baseline' : 16843548, - 'homeLayout' : 16843549, - 'opacity' : 16843550, - 'alpha' : 16843551, - 'transformPivotX' : 16843552, - 'transformPivotY' : 16843553, - 'translationX' : 16843554, - 'translationY' : 16843555, - 'scaleX' : 16843556, - 'scaleY' : 16843557, - 'rotation' : 16843558, - 'rotationX' : 16843559, - 'rotationY' : 16843560, - 'showDividers' : 16843561, - 'dividerPadding' : 16843562, - 'borderlessButtonStyle' : 16843563, - 'dividerHorizontal' : 16843564, - 'itemPadding' : 16843565, - 'buttonBarStyle' : 16843566, - 'buttonBarButtonStyle' : 16843567, - 'segmentedButtonStyle' : 16843568, - 'staticWallpaperPreview' : 16843569, - 'allowParallelSyncs' : 16843570, - 'isAlwaysSyncable' : 16843571, - 'verticalScrollbarPosition' : 16843572, - 'fastScrollAlwaysVisible' : 16843573, - 'fastScrollThumbDrawable' : 16843574, - 'fastScrollPreviewBackgroundLeft' : 16843575, - 'fastScrollPreviewBackgroundRight' : 16843576, - 'fastScrollTrackDrawable' : 16843577, - 'fastScrollOverlayPosition' : 16843578, - 'customTokens' : 16843579, - 'nextFocusForward' : 16843580, - 'firstDayOfWeek' : 16843581, - 'showWeekNumber' : 16843582, - 'minDate' : 16843583, - 'maxDate' : 16843584, - 'shownWeekCount' : 16843585, - 'selectedWeekBackgroundColor' : 16843586, - 'focusedMonthDateColor' : 16843587, - 'unfocusedMonthDateColor' : 16843588, - 'weekNumberColor' : 16843589, - 'weekSeparatorLineColor' : 16843590, - 'selectedDateVerticalBar' : 16843591, - 'weekDayTextAppearance' : 16843592, - 'dateTextAppearance' : 16843593, - 'solidColor' : 16843594, - 'spinnersShown' : 16843595, - 'calendarViewShown' : 16843596, - 'state_multiline' : 16843597, - 'detailsElementBackground' : 16843598, - 'textColorHighlightInverse' : 16843599, - 'textColorLinkInverse' : 16843600, - 'editTextColor' : 16843601, - 'editTextBackground' : 16843602, - 'horizontalScrollViewStyle' : 16843603, - 'layerType' : 16843604, - 'alertDialogIcon' : 16843605, - 'windowMinWidthMajor' : 16843606, - 'windowMinWidthMinor' : 16843607, - 'queryHint' : 16843608, - 'fastScrollTextColor' : 16843609, - 'largeHeap' : 16843610, - 'windowCloseOnTouchOutside' : 16843611, - 'datePickerStyle' : 16843612, - 'calendarViewStyle' : 16843613, - 'textEditSidePasteWindowLayout' : 16843614, - 'textEditSideNoPasteWindowLayout' : 16843615, - 'actionMenuTextAppearance' : 16843616, - 'actionMenuTextColor' : 16843617, - 'textCursorDrawable' : 16843618, - 'resizeMode' : 16843619, - 'requiresSmallestWidthDp' : 16843620, - 'compatibleWidthLimitDp' : 16843621, - 'largestWidthLimitDp' : 16843622, - 'state_hovered' : 16843623, - 'state_drag_can_accept' : 16843624, - 'state_drag_hovered' : 16843625, - 'stopWithTask' : 16843626, - 'switchTextOn' : 16843627, - 'switchTextOff' : 16843628, - 'switchPreferenceStyle' : 16843629, - 'switchTextAppearance' : 16843630, - 'track' : 16843631, - 'switchMinWidth' : 16843632, - 'switchPadding' : 16843633, - 'thumbTextPadding' : 16843634, - 'textSuggestionsWindowStyle' : 16843635, - 'textEditSuggestionItemLayout' : 16843636, - 'rowCount' : 16843637, - 'rowOrderPreserved' : 16843638, - 'columnCount' : 16843639, - 'columnOrderPreserved' : 16843640, - 'useDefaultMargins' : 16843641, - 'alignmentMode' : 16843642, - 'layout_row' : 16843643, - 'layout_rowSpan' : 16843644, - 'layout_columnSpan' : 16843645, - 'actionModeSelectAllDrawable' : 16843646, - 'isAuxiliary' : 16843647, - 'accessibilityEventTypes' : 16843648, - 'packageNames' : 16843649, - 'accessibilityFeedbackType' : 16843650, - 'notificationTimeout' : 16843651, - 'accessibilityFlags' : 16843652, - 'canRetrieveWindowContent' : 16843653, - 'listPreferredItemHeightLarge' : 16843654, - 'listPreferredItemHeightSmall' : 16843655, - 'actionBarSplitStyle' : 16843656, - 'actionProviderClass' : 16843657, - 'backgroundStacked' : 16843658, - 'backgroundSplit' : 16843659, - 'textAllCaps' : 16843660, - 'colorPressedHighlight' : 16843661, - 'colorLongPressedHighlight' : 16843662, - 'colorFocusedHighlight' : 16843663, - 'colorActivatedHighlight' : 16843664, - 'colorMultiSelectHighlight' : 16843665, - 'drawableStart' : 16843666, - 'drawableEnd' : 16843667, - 'actionModeStyle' : 16843668, - 'minResizeWidth' : 16843669, - 'minResizeHeight' : 16843670, - 'actionBarWidgetTheme' : 16843671, - 'uiOptions' : 16843672, - 'subtypeLocale' : 16843673, - 'subtypeExtraValue' : 16843674, - 'actionBarDivider' : 16843675, - 'actionBarItemBackground' : 16843676, - 'actionModeSplitBackground' : 16843677, - 'textAppearanceListItem' : 16843678, - 'textAppearanceListItemSmall' : 16843679, - 'targetDescriptions' : 16843680, - 'directionDescriptions' : 16843681, - 'overridesImplicitlyEnabledSubtype' : 16843682, - 'listPreferredItemPaddingLeft' : 16843683, - 'listPreferredItemPaddingRight' : 16843684, - 'requiresFadingEdge' : 16843685, - 'publicKey' : 16843686, - 'parentActivityName' : 16843687, - 'isolatedProcess' : 16843689, - 'importantForAccessibility' : 16843690, - 'keyboardLayout' : 16843691, - 'fontFamily' : 16843692, - 'mediaRouteButtonStyle' : 16843693, - 'mediaRouteTypes' : 16843694, - 'supportsRtl' : 16843695, - 'textDirection' : 16843696, - 'textAlignment' : 16843697, - 'layoutDirection' : 16843698, - 'paddingStart' : 16843699, - 'paddingEnd' : 16843700, - 'layout_marginStart' : 16843701, - 'layout_marginEnd' : 16843702, - 'layout_toStartOf' : 16843703, - 'layout_toEndOf' : 16843704, - 'layout_alignStart' : 16843705, - 'layout_alignEnd' : 16843706, - 'layout_alignParentStart' : 16843707, - 'layout_alignParentEnd' : 16843708, - 'listPreferredItemPaddingStart' : 16843709, - 'listPreferredItemPaddingEnd' : 16843710, - 'singleUser' : 16843711, - 'presentationTheme' : 16843712, - 'subtypeId' : 16843713, - 'initialKeyguardLayout' : 16843714, - 'widgetCategory' : 16843716, - 'permissionGroupFlags' : 16843717, - 'labelFor' : 16843718, - 'permissionFlags' : 16843719, - 'checkedTextViewStyle' : 16843720, - 'showOnLockScreen' : 16843721, - 'format12Hour' : 16843722, - 'format24Hour' : 16843723, - 'timeZone' : 16843724, - 'mipMap' : 16843725, - 'mirrorForRtl' : 16843726, - 'windowOverscan' : 16843727, - 'requiredForAllUsers' : 16843728, - 'indicatorStart' : 16843729, - 'indicatorEnd' : 16843730, - 'childIndicatorStart' : 16843731, - 'childIndicatorEnd' : 16843732, - 'restrictedAccountType' : 16843733, - 'requiredAccountType' : 16843734, - 'canRequestTouchExplorationMode' : 16843735, - 'canRequestEnhancedWebAccessibility' : 16843736, - 'canRequestFilterKeyEvents' : 16843737, - 'layoutMode' : 16843738, - 'keySet' : 16843739, - 'targetId' : 16843740, - 'fromScene' : 16843741, - 'toScene' : 16843742, - 'transition' : 16843743, - 'transitionOrdering' : 16843744, - 'fadingMode' : 16843745, - 'startDelay' : 16843746, - 'ssp' : 16843747, - 'sspPrefix' : 16843748, - 'sspPattern' : 16843749, - 'addPrintersActivity' : 16843750, - 'vendor' : 16843751, - 'category' : 16843752, - 'isAsciiCapable' : 16843753, - 'autoMirrored' : 16843754, - 'supportsSwitchingToNextInputMethod' : 16843755, - 'requireDeviceUnlock' : 16843756, - 'apduServiceBanner' : 16843757, - 'accessibilityLiveRegion' : 16843758, - 'windowTranslucentStatus' : 16843759, - 'windowTranslucentNavigation' : 16843760, - 'advancedPrintOptionsActivity' : 16843761, - 'banner' : 16843762, - 'windowSwipeToDismiss' : 16843763, - 'isGame' : 16843764, - 'allowEmbedded' : 16843765, - 'setupActivity' : 16843766, - 'fastScrollStyle' : 16843767, - 'windowContentTransitions' : 16843768, - 'windowContentTransitionManager' : 16843769, - 'translationZ' : 16843770, - 'tintMode' : 16843771, - 'controlX1' : 16843772, - 'controlY1' : 16843773, - 'controlX2' : 16843774, - 'controlY2' : 16843775, - 'transitionName' : 16843776, - 'transitionGroup' : 16843777, - 'viewportWidth' : 16843778, - 'viewportHeight' : 16843779, - 'fillColor' : 16843780, - 'pathData' : 16843781, - 'strokeColor' : 16843782, - 'strokeWidth' : 16843783, - 'trimPathStart' : 16843784, - 'trimPathEnd' : 16843785, - 'trimPathOffset' : 16843786, - 'strokeLineCap' : 16843787, - 'strokeLineJoin' : 16843788, - 'strokeMiterLimit' : 16843789, - 'colorControlNormal' : 16843817, - 'colorControlActivated' : 16843818, - 'colorButtonNormal' : 16843819, - 'colorControlHighlight' : 16843820, - 'persistableMode' : 16843821, - 'titleTextAppearance' : 16843822, - 'subtitleTextAppearance' : 16843823, - 'slideEdge' : 16843824, - 'actionBarTheme' : 16843825, - 'textAppearanceListItemSecondary' : 16843826, - 'colorPrimary' : 16843827, - 'colorPrimaryDark' : 16843828, - 'colorAccent' : 16843829, - 'nestedScrollingEnabled' : 16843830, - 'windowEnterTransition' : 16843831, - 'windowExitTransition' : 16843832, - 'windowSharedElementEnterTransition' : 16843833, - 'windowSharedElementExitTransition' : 16843834, - 'windowAllowReturnTransitionOverlap' : 16843835, - 'windowAllowEnterTransitionOverlap' : 16843836, - 'sessionService' : 16843837, - 'stackViewStyle' : 16843838, - 'switchStyle' : 16843839, - 'elevation' : 16843840, - 'excludeId' : 16843841, - 'excludeClass' : 16843842, - 'hideOnContentScroll' : 16843843, - 'actionOverflowMenuStyle' : 16843844, - 'documentLaunchMode' : 16843845, - 'maxRecents' : 16843846, - 'autoRemoveFromRecents' : 16843847, - 'stateListAnimator' : 16843848, - 'toId' : 16843849, - 'fromId' : 16843850, - 'reversible' : 16843851, - 'splitTrack' : 16843852, - 'targetName' : 16843853, - 'excludeName' : 16843854, - 'matchOrder' : 16843855, - 'windowDrawsSystemBarBackgrounds' : 16843856, - 'statusBarColor' : 16843857, - 'navigationBarColor' : 16843858, - 'contentInsetStart' : 16843859, - 'contentInsetEnd' : 16843860, - 'contentInsetLeft' : 16843861, - 'contentInsetRight' : 16843862, - 'paddingMode' : 16843863, - 'layout_rowWeight' : 16843864, - 'layout_columnWeight' : 16843865, - 'translateX' : 16843866, - 'translateY' : 16843867, - 'selectableItemBackgroundBorderless' : 16843868, - 'elegantTextHeight' : 16843869, - 'searchKeyphraseId' : 16843870, - 'searchKeyphrase' : 16843871, - 'searchKeyphraseSupportedLocales' : 16843872, - 'windowTransitionBackgroundFadeDuration' : 16843873, - 'overlapAnchor' : 16843874, - 'progressTint' : 16843875, - 'progressTintMode' : 16843876, - 'progressBackgroundTint' : 16843877, - 'progressBackgroundTintMode' : 16843878, - 'secondaryProgressTint' : 16843879, - 'secondaryProgressTintMode' : 16843880, - 'indeterminateTint' : 16843881, - 'indeterminateTintMode' : 16843882, - 'backgroundTint' : 16843883, - 'backgroundTintMode' : 16843884, - 'foregroundTint' : 16843885, - 'foregroundTintMode' : 16843886, - 'buttonTint' : 16843887, - 'buttonTintMode' : 16843888, - 'thumbTint' : 16843889, - 'thumbTintMode' : 16843890, - 'fullBackupOnly' : 16843891, - 'propertyXName' : 16843892, - 'propertyYName' : 16843893, - 'relinquishTaskIdentity' : 16843894, - 'tileModeX' : 16843895, - 'tileModeY' : 16843896, - 'actionModeShareDrawable' : 16843897, - 'actionModeFindDrawable' : 16843898, - 'actionModeWebSearchDrawable' : 16843899, - 'transitionVisibilityMode' : 16843900, - 'minimumHorizontalAngle' : 16843901, - 'minimumVerticalAngle' : 16843902, - 'maximumAngle' : 16843903, - 'searchViewStyle' : 16843904, - 'closeIcon' : 16843905, - 'goIcon' : 16843906, - 'searchIcon' : 16843907, - 'voiceIcon' : 16843908, - 'commitIcon' : 16843909, - 'suggestionRowLayout' : 16843910, - 'queryBackground' : 16843911, - 'submitBackground' : 16843912, - 'buttonBarPositiveButtonStyle' : 16843913, - 'buttonBarNeutralButtonStyle' : 16843914, - 'buttonBarNegativeButtonStyle' : 16843915, - 'popupElevation' : 16843916, - 'actionBarPopupTheme' : 16843917, - 'multiArch' : 16843918, - 'touchscreenBlocksFocus' : 16843919, - 'windowElevation' : 16843920, - 'launchTaskBehindTargetAnimation' : 16843921, - 'launchTaskBehindSourceAnimation' : 16843922, - 'restrictionType' : 16843923, - 'dayOfWeekBackground' : 16843924, - 'dayOfWeekTextAppearance' : 16843925, - 'headerMonthTextAppearance' : 16843926, - 'headerDayOfMonthTextAppearance' : 16843927, - 'headerYearTextAppearance' : 16843928, - 'yearListItemTextAppearance' : 16843929, - 'yearListSelectorColor' : 16843930, - 'calendarTextColor' : 16843931, - 'recognitionService' : 16843932, - 'timePickerStyle' : 16843933, - 'timePickerDialogTheme' : 16843934, - 'headerTimeTextAppearance' : 16843935, - 'headerAmPmTextAppearance' : 16843936, - 'numbersTextColor' : 16843937, - 'numbersBackgroundColor' : 16843938, - 'numbersSelectorColor' : 16843939, - 'amPmTextColor' : 16843940, - 'amPmBackgroundColor' : 16843941, - 'searchKeyphraseRecognitionFlags' : 16843942, - 'checkMarkTint' : 16843943, - 'checkMarkTintMode' : 16843944, - 'popupTheme' : 16843945, - 'toolbarStyle' : 16843946, - 'windowClipToOutline' : 16843947, - 'datePickerDialogTheme' : 16843948, - 'showText' : 16843949, - 'windowReturnTransition' : 16843950, - 'windowReenterTransition' : 16843951, - 'windowSharedElementReturnTransition' : 16843952, - 'windowSharedElementReenterTransition' : 16843953, - 'resumeWhilePausing' : 16843954, - 'datePickerMode' : 16843955, - 'timePickerMode' : 16843956, - 'inset' : 16843957, - 'letterSpacing' : 16843958, - 'fontFeatureSettings' : 16843959, - 'outlineProvider' : 16843960, - 'contentAgeHint' : 16843961, - 'country' : 16843962, - 'windowSharedElementsUseOverlay' : 16843963, - 'reparent' : 16843964, - 'reparentWithOverlay' : 16843965, - 'ambientShadowAlpha' : 16843966, - 'spotShadowAlpha' : 16843967, - 'navigationIcon' : 16843968, - 'navigationContentDescription' : 16843969, - 'fragmentExitTransition' : 16843970, - 'fragmentEnterTransition' : 16843971, - 'fragmentSharedElementEnterTransition' : 16843972, - 'fragmentReturnTransition' : 16843973, - 'fragmentSharedElementReturnTransition' : 16843974, - 'fragmentReenterTransition' : 16843975, - 'fragmentAllowEnterTransitionOverlap' : 16843976, - 'fragmentAllowReturnTransitionOverlap' : 16843977, - 'patternPathData' : 16843978, - 'strokeAlpha' : 16843979, - 'fillAlpha' : 16843980, - 'windowActivityTransitions' : 16843981, - 'colorEdgeEffect' : 16843982 - } -} - -SYSTEM_RESOURCES = { - "attributes": { - "forward": {k: v for k, v in resources['attr'].iteritems()}, - "inverse": {v: k for k, v in resources['attr'].iteritems()} - }, - "styles": { - "forward": {k: v for k, v in resources['style'].iteritems()}, - "inverse": {v: k for k, v in resources['style'].iteritems()} - } -} From 71bdde81402903bba32784b9e408ae01e4664e48 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 7 Jun 2017 15:44:47 +0800 Subject: [PATCH 27/67] show apk package and version --- tools/packer-ng-v2.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index 8e63b44..d0ec43a 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -2,7 +2,7 @@ # @Author: mcxiaoke # @Date: 2017-06-06 14:03:18 # @Last Modified by: mcxiaoke -# @Last Modified time: 2017-06-07 14:58:50 +# @Last Modified time: 2017-06-07 15:42:07 from __future__ import print_function # from __future__ import unicode_literals import os @@ -56,17 +56,6 @@ logger.debug('AUTHOR:%s', AUTHOR) logger.debug('VERSION:%s', VERSION) -APK1 = 'apks/Cat/packer-ng-release-v1.7.1-SNAPSHOT-田园猫.apk' -APK2 = 'apks/Cat/packer-ng-release-v1.7.1-SNAPSHOT-Special@Cat%001.apk' -APK3 = 'apks/Fish/packer-ng-release-v1.7.1-SNAPSHOT-2017年.apk' -APK_RELEASE = 'apks/sample-Cat-release.apk' -APK_BETA = 'apks/sample-Cat-beta.apk' -APK_DEBUG = 'apks/sample-Cat-debug.apk' -ZIP_NOT_APK = 'cli.zip' -TXT_NOT_APK = 'cv.java' - -APK = APK1 - class ZipFormatException(Exception): pass @@ -162,7 +151,7 @@ def findPluginBlockValues(apk): def findApkSigningBlock(apk): zp = zipfile.ZipFile(apk) zp.testzip() - with open(apk, "r+b") as f: + with open(apk, "rb") as f: mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) sections = findZipSections(mm) @@ -376,9 +365,14 @@ def to_hex(s): if len(sys.argv) < 2: print('Usage: {} app.apk'.format(prog)) sys.exit(1) + apk = os.path.abspath(sys.argv[1]) + from apk import APK + info = APK(apk) try: - apk = os.path.abspath(sys.argv[1]) - print('File: \t{}'.format(os.path.basename(apk))) + print('File: \t\t{}'.format(os.path.basename(apk))) + print('Package: \t{}'.format(info.get_package())) + print('Version: \t{}'.format(info.get_version_name())) + print('Build: \t\t{}'.format(info.get_version_code())) channel = getChannel(apk) print('Channel: \t{}'.format(channel)) except Exception as e: From dc5b4b71dbaabd4ffcf1a58cd02b9154cd4caff4 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 7 Jun 2017 15:56:23 +0800 Subject: [PATCH 28/67] minor fix packer-ng script --- tools/packer-ng-v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index d0ec43a..3d514e1 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -2,7 +2,7 @@ # @Author: mcxiaoke # @Date: 2017-06-06 14:03:18 # @Last Modified by: mcxiaoke -# @Last Modified time: 2017-06-07 15:42:07 +# @Last Modified time: 2017-06-07 15:55:31 from __future__ import print_function # from __future__ import unicode_literals import os @@ -366,7 +366,7 @@ def to_hex(s): print('Usage: {} app.apk'.format(prog)) sys.exit(1) apk = os.path.abspath(sys.argv[1]) - from apk import APK + from apkinfo import APK info = APK(apk) try: print('File: \t\t{}'.format(os.path.basename(apk))) From 033e71c90149f1ba1319dd92545e6ddc1253d83b Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 7 Jun 2017 17:13:40 +0800 Subject: [PATCH 29/67] add docs for v2 to readme --- .../com/mcxiaoke/packer/ng/Const.groovy | 7 +- readme.md | 258 ++++++++---------- 2 files changed, 120 insertions(+), 145 deletions(-) diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy index 13d82bb..137bc18 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/Const.groovy @@ -20,13 +20,12 @@ class Const { * 1. projectName * 2. appName * 3. appPkg - * 4. buildType - * 5. channel + * 4. channel + * 5. buildType * 6. versionName * 7. versionCode * 8. buildTime - * 9. fileMD5 - * 10. fileSHA1 + * 9. fileSHA1 * * default value: '${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}' */ diff --git a/readme.md b/readme.md index e19302d..a3a14cb 100644 --- a/readme.md +++ b/readme.md @@ -1,29 +1,18 @@ -下一代Android渠道打包工具 +PackerNg V2 ======== +极速渠道打包工具 ## 特别提示 -- **请使用最新版本的PackerNg,如果使用的Android Gradle Plugin版本大于2.2.0,请务必在 `signingConfigs` 里增加 `v2SigningEnabled false` 禁用新版签名模式,详细的说明见这里:[兼容性问题说明](compatibility.md)。** - -- **如果你同时使用 [tinker](https://github.com/Tencent/tinker) ,请使用`1.0.9`以后的版本,同时阅读tinker的文档,确保没有兼容问题。** +V2版只支持`APK Signature Scheme v2`,要求在 `signingConfigs` 里 `v2SigningEnabled true` 启用新版签名模式,并使用`2.2.0`以上版本的Gradle插件,如果你需要使用旧版本,看这里 [v1.0.9](https://github.com/mcxiaoke/packer-ng-plugin/tree/v1.0.9)。 ## 最新版本 -- **v1.0.9 - 2017.03.03** - 获取APK文件路径时优先读取sourceDir -- **v1.0.8 - 2016.10.20** - 移除对旧版打包工具的扩展属性兼容 -- **v1.0.7 - 2016.08.09** - 优化签名校验和渠道写入,完善异常处理 -- **v1.0.6 - 2016.08.05** - V2签名模式兼容问题提示,打包脚本优化 -- **v1.0.5 - 2016.05.30** - 签名检查调整为可选,文件名模板支持MD5和SHA1 -- **v1.0.4 - 2016.01.19** - 完善获取APK路径的方法,增加MarketInfo -- **v1.0.3 - 2016.01.14** - 增加缓存,新增ResUtils,更有好的错误提示 -- **v1.0.2 - 2015.12.04** - 兼容productFlavors,完善异常处理 -- **v1.0.1 - 2015.12.01** - 如果没有读取到渠道,默认返回空字符串 -- **v1.0.0 - 2015.11.30** - 增加Java和Python打包脚本,增加文档 -- **v0.9.9 - 2015.11.26** - 测试版发布,支持全新的极速打包方式 +- **v2.0.0 - 2017.06.16** - 全新发布,支持V2签名模式,包含多项优化 ## 项目介绍 -[**packer-ng-plugin**](https://github.com/mcxiaoke/packer-ng-plugin) 是下一代Android渠道打包工具Gradle插件,支持极速打包,**100**个渠道包只需要**10**秒钟,速度是 [**gradle-packer-plugin**](https://github.com/mcxiaoke/gradle-packer-plugin) 的**300**倍以上,可方便的用于CI系统集成,支持自定义输出目录和最终APK文件名,依赖包: `com.mcxiaoke.gradle:packer-ng:1.0.9` 简短名:`packer`,可以在项目的 `build.gradle` 中指定使用,还提供了命令行独立使用的Java和Python脚本。实现原理见本文末尾。 +[**packer-ng-plugin**](https://github.com/mcxiaoke/packer-ng-plugin) 是下一代Android渠道打包工具Gradle插件,支持极速打包,**100**个渠道包只需要**10**秒钟,速度是 [**gradle-packer-plugin**](https://github.com/mcxiaoke/gradle-packer-plugin) 的**300**倍以上,可方便的用于CI系统集成。 ## 使用指南 @@ -36,8 +25,7 @@ buildscript { ...... dependencies{ - // add packer-ng - classpath 'com.mcxiaoke.gradle:packer-ng:1.0.9' + classpath 'com.mcxiaoke.packer-ng:plugin:2.0.0' } } ``` @@ -48,181 +36,169 @@ buildscript { apply plugin: 'packer' dependencies { - compile 'com.mcxiaoke.gradle:packer-helper:1.0.9' + compile 'com.mcxiaoke.packer-ng:helper:2.0.0' } +``` + +**注意:`plugin` 和 `helper` 的版本号需要保持一致** - android { - //... - signingConfigs { - release { - // 满足下面两个条件时需要此配置 - // 1. Gradle版本 >= 2.14.1 - // 2. Android Gradle Plugin 版本 >= 2.2.0 - // 作用是只使用旧版签名,禁用V2版签名模式 - v2SigningEnabled false - } - } - } +### 插件配置示例 + +``` +//packer-begin +packer { + archiveNameFormat = '${buildType}-v${versionName}-${channel}' + archiveOutput = new File(project.rootProject.buildDir, "apks") +// channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', +// 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] +// channelFile = new File(project.rootDir, "markets.txt") + channelMap = [ + "Cat" : project.rootProject.file("channels/cat.txt"), + "Dog" : project.rootProject.file("channels/dog.txt"), + "Fish": project.rootProject.file("channels/channels.txt") + ] +} +//packer-end ``` -**注意:`packer-ng` 和 `packer-helper` 的版本号需要保持一致** +* **archiveNameFormat** - 指定最终输出的渠道包文件名的格式模版,详细说明见后面,默认值是 `${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}` (可选) +* **archiveOutput** - 指定最终输出的渠道包的存储位置,默认值是 `${project.buildDir}/archives` (可选) +* **channelList** - 指定渠道列表,List类型,见示例 +* **channelMap** - 根据productFlavor指定不同的渠道列表文件,见示例 +* **channelFile** - 指定渠道列表文件,File类型,见示例 -### Java代码中获取当前渠道 +注意:`channelList` / `channelMap` / `channelFile` 不能同时使用,根据实际情况选择一种即可,三个属性同时存在时优先级为: `channelList` > `channelMap` > `channelFile `,另外,这三个属性会被命令行参数 `-Pchannels` 覆盖。 -提示:`PackerNg.getMarket(Context)`内部缓存了结果,不会重复解析APK文件 +### 渠道列表格式 -```java +渠道名列表文件是纯文本文件,按行读取,每行一个渠道,行首和行尾的空白会被忽略,如果有注释,渠道名和注释之间用 `#` 分割。 -// 如果没有使用PackerNg打包添加渠道,默认返回的是"" -// com.mcxiaoke.packer.helper.PackerNg -final String market = PackerNg.getMarket(Context) -// 或者使用 PackerNg.getMarket(Context,defaultValue) -// 之后就可以使用了,比如友盟可以这样设置 -MobclickAgent.startWithConfigure(new MobclickAgent.UMAnalyticsConfig(context, umeng_appkey, market)); +渠道名建议尽量使用规范的**中英文和数字**,不要使用特殊字符和不可见字符。示例: ``` +Google_Play#play store market +Gradle_Test# 这是注释 +SomeMarket#some market +中文渠道 # comments +HelloWorld -### Gradle打包说明 +``` -可以通过两种方式指定 `market` 属性,根据需要选用: +### 集成打包 + +* 需要打包 `release` 类型时,最简单的命令如下: -- 打包时命令行使用 `-Pmarket= yourMarketFilePath` 指定属性 -- 在 `gradle.properties` 里加入 `market=yourMarketFilePath` + ```shell + ./gradlew clean apkRelease + ``` -market是你的渠道名列表文件,market文件是基于**项目根目录**的 `相对路径` ,假设你的项目位于 `~/github/myapp` 你的market文件位于 `~/github/myapp/config/markets.txt` 那么参数应该是 `-Pmarket=config/markets.txt`,一般建议直接放在项目根目录,如果market文件参数错误或者文件不存在会抛出异常。 +* 项目中使用了 `productFlavors` -渠道名列表文件是纯文本文件,每行一个渠道号,列表解析的时候会自动忽略空白行和格式不规范的行,请注意看命令行输出,渠道名和注释之间用 `#` 号分割开,可以没有注释,示例: + 如果项目中指定了多个 `flavor` ,需要指定需要打渠道包的 `flavor` 名字,假设你有 `Paid` `Free` 两个 `flavor` ,打包的时候命令如下: -``` - Google_Play#play store market - Gradle_Test#test - SomeMarket#some market - HelloWorld -``` + ```shell + ./gradlew clean apkPaidRelease + ./gradlew clean apkFreeRelease + ``` + + 直接使用 `./gradlew clean apkRelease` 会输出所有 `flavor` 的渠道包。 -渠道打包的Gradle命令行参数格式示例(在项目根目录执行): +* 通过参数直接指定渠道列表(会覆盖`build.gradle`中的属性): -```shell -./gradlew -Pmarket=markets.txt clean apkRelease -``` + ```shell + ./gradlew clean apkRelease -Pchannels=ch1,ch2,douban,google + ``` + + 渠道数目很少时可以使用此种方式。 + +* 通过参数指定渠道列表文件的位置(会覆盖`build.gradle`中的属性): + + ```shell + ./gradlew clean apkRelease -Pchannels=@channels.txt + ``` + + 使用@符号指定渠道列表文件的位置,使用相对于项目根目录的相对路径。 -打包完成后你可以在 `${项目根目录}/build/archives/` 目录找到最终的渠道包。 +* 还可以指定输出目录和文件名格式模版: -#### 任务说明 + ```shell + ./gradlew clean apkRelease -Poutput=build/apks + ./gradlew clean apkRelease -Pformat=${versionName}-${channel} + ``` + + 这些参数 `channels` `output` `format` 可以组合使用,命令行参数会覆盖 `build.gradle` 对应的属性。 -渠道打包的Gradle Task名字是 `apk${buildType}` buildType一般是release,也可以是你自己指定的beta或者someOtherType,使用时首字母需要大写,例如release的渠道包任务名是 `apkRelease`,beta的渠道包任务名是 `apkBeta`,其它的以此类推。 +* Gradle打包命令说明 -#### 注意事项 + 渠道打包的Task名字是 `apk${flavor}${buildType}` buildType一般是release,也可以是你自己指定的beta或者someOtherType,如果没有 `flavor` 可以忽略,使用时首字母需要大写,假设 `flavor` 是 `Paid`,`release`类型对应的任务名是 `apkPaidRelease`,`beta`类型对应的任务名是 `apkPaidBetaBeta`,其它的以此类推。 + +* 特别提示 -**不支持`productFlavors`中定义的条件编译变量,不支持修改AndroidManifest** + 如果你同时使用其它的资源压缩工具或应用加固功能,请使用命令行脚本打包增加渠道信息,增加渠道信息需要放在APK处理过程的最后一步。 + +### 脚本打包 -如果你的项目有多个`productFlavors`,默认只会用第一个`flavor`生成的APK文件作为打包工具的输入参数,忽略其它`flavor`生成的apk,代码里用的是 `theVariant.outputs[0].outputFile`。如果你想指定使用某个flavor来生成渠道包,可以用 `apkFlavor1Release`,`apkFlavor2Beta`这样的名字,示例(假设flavor名字是Intel): +除了使用Gradle集成以外,还可以使用项目提供的Java脚本打包,Jar位于本项目的 `tools` 目录,请使用最新版,以下用 `packer-ng` 指代 `java -jar tools/packer-ng-2.0.0.jar`,示例: + +* 直接指定渠道列表打包: ```shell -./gradlew -Pmarket=markets.txt clean apkIntelRelease -``` +packer-ng generate --channels=ch1,ch2,ch3 --output=build/archives app.apk +``` + +* 指定渠道列表文件打包: -### 命令行打包说明 +```shell +packer-ng generate --channels=@channels.txt --output=build/archives app.apk +``` -**特别提示:如果你同时使用其它的资源压缩工具或应用加固功能,请使用命令行脚本打包增加渠道信息,增加渠道信息需要放在APK处理过程的最后一步。** +* 验证渠道包的渠道信息: -如果不想使用Gradle插件,这里还有两个命令行打包脚本,在项目的 `tools` 目录里,分别是 `PackerNg-1.0.9.jar` 和 `PackerNg-1.0.9.py`,使用命令行打包工具,在Java代码里仍然是使用`helper`包里的 `PackerNg.getMarket(Context)` 读取渠道。 +```shell +packer-ng verify app.apk +``` -#### Java脚本 +* 其它参数可以运行命令查看帮助 ```shell -java -jar PackerNg-x.x.x.jar apkFile marketFile outputDir +java -jar tools/packer-ng-2.0.0.jar --help ``` -#### Python脚本 +* 还可以使用Python脚本读取渠道: ```shell -python PackerNg-x.x.x.py [file] [market] [output] [-h] [-s] [-t TEST] +python tools/packer-ng-v2.py app.apk ``` -#### 不使用Gradle -使用命令行打包脚本,不想添加Gradle依赖的,可以完全忽略Gradle的配置,直接复制 [PackerNg.java](helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java) 到项目中使用即可。 +### 代码中读取渠道 -### 插件配置说明(可选) +`PackerNg.getMarket(Context)`内部缓存了结果,不会重复解析 -```groovy -packer { - // 指定渠道打包输出目录 - // archiveOutput = file(new File(project.rootProject.buildDir.path, "archives")) - // 指定渠道打包输出文件名格式 - // 默认是 `${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}` - // archiveNameFormat = '' - // 是否检查Gradle配置中的signingConfig,默认不检查 - // checkSigningConfig = false - // 是否检查Gradle配置中的zipAlignEnabled,默认不检查 - // checkZipAlign = false -} +```java +// 如果没有找到渠道信息,默认返回的是"" +// com.mcxiaoke.packer.helper.PackerNg +String market = PackerNg.getMarket(Context) ``` -举例:假如你的App包名是 `com.your.company` ,渠道名是 `Google_Play` ,`buildType` 是 `release` ,`versionName` 是 `2.1.15` ,`versionCode` 是 `200115` ,那么生成的APK的文件名是 `com.your.company-Google_Player-release-2.1.15-20015.apk` +### 文件名格式模版 + +格式模版使用Groovy字符串模版引擎,默认文件名格式是: `${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}` 。 -* **archiveOutput** 指定渠道打包输出的APK存放目录,默认位于`${项目根目录}/build/archives` +假如你的App包名是 `com.your.company` ,渠道名是 `Google_Play` ,`buildType` 是 `release` ,`versionName` 是 `2.1.15` ,`versionCode` 是 `200115` ,那么生成的默认APK的文件名是 `com.your.company-Google_Player-release-2.1.15-20015.apk` 。 -* **archiveNameFormat** - `Groovy格式字符串`, 指定渠道打包输出的APK文件名格式,默认文件名格式是: `${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}`,可使用以下变量: +可使用以下变量: * *projectName* - 项目名字 * *appName* - App模块名字 * *appPkg* - `applicationId` (App包名packageName) + * *channel* - 打包时指定的渠道名 * *buildType* - `buildType` (release/debug/beta等) - * *flavorName* - `flavorName` (对应渠道打包中的渠道名字) + * *flavorName* - `flavorName` (flavor名字,如paid/free等) * *versionName* - `versionName` (显示用的版本号) * *versionCode* - `versionCode` (内部版本号) * *buildTime* - `buildTime` (编译构建日期时间) - * *fileMD5* - `fileMD5 ` (最终APK文件的MD5哈希值) * *fileSHA1* - `fileSHA1 ` (最终APK文件的SHA1哈希值) -## 实现原理 - -### PackerNg原理 - -#### 优点 - -- 使用APK注释字段保存渠道信息和MAGIC字节,从文件末尾读取渠道信息,速度快 -- 实现为一个Gradle Plugin,支持定制输出APK的文件名等信息,方便CI集成 -- 提供Java版和Python的独立命令行脚本,不依赖Gradle插件,支持独立使用 -- 由于打包速度极快,单个包只需要5毫秒左右,可用于网站后台动态生成渠道包 - -#### 缺点 - -- 没有使用Android的productFlavors,无法利用flavors条件编译的功能 - -### 文件格式 - -Android应用使用的APK文件就是一个带签名信息的ZIP文件,根据 [ZIP文件格式规范](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT),每个ZIP文件的最后都必须有一个叫 [Central Directory Record](https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html) 的部分,这个CDR的最后部分叫"end of central directory record",这一部分包含一些元数据,它的末尾是ZIP文件的注释。注释包含**Comment Length**和**File Comment**两个字段,前者表示注释内容的长度,后者是注释的内容,正确修改这一部分不会对ZIP文件造成破坏,利用这个字段,我们可以添加一些自定义的数据,PackerNg项目就是在这里添加和读取渠道信息。 - -### 细节处理 - -原理很简单,就是将渠道信息存放在APK文件的注释字段中,但是实现起来遇到不少坑,测试了好多次。使用Java写入APK文件注释虽然可以正常读取,但是安装的时候会失败,Java的Zip实现写入了某些特殊字符导致APK文件校验失败,于是只能放弃这个方法。同样的功能使用Python测试完全没有问题,处理后的APK可以正常安装。Java 7里可以使用 `zipFile.getComment()` 方法直接读取注释,非常方便。但是Android系统直到API 19,也就是4.4以上的版本才支持 `ZipFile.getComment()` 方法。由于要兼容之前的版本,所以这个方法也不能使用。 - -#### 解决方法 - -由于使用Java直接写入和读取ZIP文件的注释都不可行,使用Python又不方便与Gradle系统集成,所以只能自己实现注释的写入和读取。实现起来也不复杂,就是为了提高性能,避免读取整个文件,需要在注释的最后加入几个MAGIC字节,这样从文件的最后开始,读取很少的几个字节就可以定位渠道名的位置。 - -几个常量定义: - -```java -// ZIP文件的注释最长65535个字节 -static final int ZIP_COMMENT_MAX_LENGTH = 65535; -// ZIP文件注释长度字段的字节数 -static final int SHORT_LENGTH = 2; -// 文件最后用于定位的MAGIC字节 -static final byte[] MAGIC = new byte[]{0x21, 0x5a, 0x58, 0x4b, 0x21}; //!ZXK! - -``` - -#### Gradle Plugin - -这个和旧版插件基本一致,首先是读取渠道列表文件,保存起来,打包的时候遍历列表,复制生成的APK文件到临时文件,给临时文件写入渠道信息,然后复制到输出目录,文件名可以使用模板定制。详细的实现可以查看文件 [PackerNgPlugin.groovy](plugin/src/main/groovy/com/mcxiaoke/packer/ng/PackerNgPlugin.groovy) 和文件 [ArchiveAllApkTask.groovy](plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy) - -### 同类工具 - -- [**gradle-packer-plugin**](https://github.com/mcxiaoke/gradle-packer-plugin) - 旧版渠道打包工具,完全使用Gradle系统实现,能利用Android提供的productFlavors系统的条件编译功能,无任何兼容性问题,方便集成,但是由于每次都要重新打包,速度比较慢,不适合需要大量打包的情况。(性能:200个渠道包需要一到两小时) - ------ ## 关于作者 @@ -230,7 +206,7 @@ static final byte[] MAGIC = new byte[]{0x21, 0x5a, 0x58, 0x4b, 0x21}; //!ZXK! #### 联系方式 * Blog: * Github: -* Email: [github@mcxiaoke.com](mailto: github@mcxiaoke.com) +* Email: [packer-ng-plugin@mcxiaoke.com](mailto: packer-ng-plugin@mcxiaoke.com) #### 开源项目 @@ -249,7 +225,7 @@ static final byte[] MAGIC = new byte[]{0x21, 0x5a, 0x58, 0x4b, 0x21}; //!ZXK! ## License - Copyright 2014, 2015, 2016 Xiaoke Zhang + Copyright 2014 - 2017 Xiaoke Zhang Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From a4adee7fa38156a27db290df1a8f17c2874fd143 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 7 Jun 2017 17:26:18 +0800 Subject: [PATCH 30/67] release v1.8.0 for test --- gradle.properties | 2 +- sample/build.gradle | 2 +- tools/PackerNg-1.0.9.py | 247 -------------------------------------- tools/packer-ng-1.8.0.jar | Bin 0 -> 223287 bytes 4 files changed, 2 insertions(+), 249 deletions(-) delete mode 100755 tools/PackerNg-1.0.9.py create mode 100644 tools/packer-ng-1.8.0.jar diff --git a/gradle.properties b/gradle.properties index 5949ac1..0df7616 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.7.1-SNAPSHOT +VERSION_NAME=1.8.0 VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/sample/build.gradle b/sample/build.gradle index a869313..d183329 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.7.1-SNAPSHOT' + ext.packer_version = '1.8.0-SNAPSHOT' repositories { maven { url '/tmp/repo/' } diff --git a/tools/PackerNg-1.0.9.py b/tools/PackerNg-1.0.9.py deleted file mode 100755 index 81b6e76..0000000 --- a/tools/PackerNg-1.0.9.py +++ /dev/null @@ -1,247 +0,0 @@ -# -*- coding: utf-8 -*- -# @Author: mcxiaoke -# @Date: 2015-11-26 16:52:55 -# @Last Modified by: mcxiaoke -# @Last Modified time: 2017-03-02 18:30:36 -from __future__ import print_function -import os -import sys -import struct -import shutil -import argparse -import time -from apkinfo import APK -from string import Template - -__version__ = '1.0.9' # 2017.03.03 - -ZIP_SHORT = 2 -MARKET_PATH = 'markets.txt' -OUTPUT_PATH = 'apks' -ARCHIVE_FORMAT = '${name}-${package}-v${vname}-${vcode}-${market}${ext}' -MAGIC = '!ZXK!' - -INTRO_TEXT = "\nAttention: if your app using Android gradle plugin 2.2.0 or later, \ -be sure to install one of the generated Apks to device or emulator, \ -to ensure the apk can be installed without errors. \ -More details please go to github \ -https://github.com/mcxiaoke/packer-ng-plugin.\n" - - -def write_market(path, market, output, format): - ''' - write market info to apk file - write_market(apk-file-path, market-name, output-path) - ''' - path = os.path.abspath(path) - output = unicode(output) - if not os.path.exists(path): - print('apk file', path, 'not exists') - return - if read_market(path): - print('apk file', path, 'had market already') - return - if not output: - output = os.path.dirname(path) - if not os.path.exists(output): - os.makedirs(output) - name, ext = os.path.splitext(os.path.basename(path)) - # name,package,vname,vcode - app = parse_apk(path) - - dic = dict( - name=name, - package=app['app_package'], - vname=app['version_name'], - vcode=app['version_code'], - market=market.decode('utf8'), - ext=ext) - tpl = Template(format) - apk_name=tpl.substitute(dic) - - # apk_name = '%s-%s-%s-%s%s' % (app['app_package'], market.decode('utf8'), app['version_name'], app['version_code'], ext) - # apk_name = name + "-" + market + ext - apk_file = os.path.join(output, apk_name) - shutil.copy(path, apk_file) - # print('apk file:',apk_file) - index = os.stat(apk_file).st_size - index -= ZIP_SHORT - with open(apk_file, "r+b") as f: - f.seek(index) - # write comment length - f.write(struct.pack(' 0: - run_test(apkfile, test) - return - if not os.path.exists(marketfile): - print('marketfile file', marketfile, 'not exists or not readable.') - return - old_market = read_market(apkfile) - if old_market: - print('apk file', apkfile, 'already had market:', old_market, - 'please using original release apk file') - return - process(apkfile, marketfile, output, format) - - -def _parse_args(): - ''' - parse command line arguments - ''' - parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - description='PackerNg v{0} created by mcxiaoke.\n {1}'.format(__version__, INTRO_TEXT), - epilog='') - parser.add_argument('apkfile', nargs='?', - help='original release apk file path (required)') - parser.add_argument('marketfile', nargs='?', default=MARKET_PATH, - help='markets file path [default: ./markets.txt]') - parser.add_argument('output', nargs='?', default=OUTPUT_PATH, - help='archives output path [default: ./archives]') - parser.add_argument('-f', '--format', nargs='?', default=ARCHIVE_FORMAT, const=True, - help="archive format [default:'${name}-${package}-v${vname}-${vcode}-${market}${ext}']") - parser.add_argument('-s', '--show', action='store_const', const=True, - help='show apk file info (pkg/market/version)') - parser.add_argument('-t', '--test', default=0, type=int, - help='perform serval times packer-ng test') - args = parser.parse_args() - if len(sys.argv) == 1: - parser.print_help() - return None - return args - -if __name__ == '__main__': - args = _parse_args() - if args: - _check(**vars(args)) diff --git a/tools/packer-ng-1.8.0.jar b/tools/packer-ng-1.8.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..8c6bcaf6349b6cfae9cc63e42d17bae024aefbcb GIT binary patch literal 223287 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⁣qZB=X(aPH!Es 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

OoFCj?RrclrKYyS$B!e;7F|33vwVKWok=qF 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 twG&p3`W@_Y(9OVq`_~G2wu>+%`z~-o3>j` Date: Wed, 7 Jun 2017 17:42:47 +0800 Subject: [PATCH 31/67] minor fix readme --- readme.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/readme.md b/readme.md index a3a14cb..bc17d22 100644 --- a/readme.md +++ b/readme.md @@ -23,7 +23,6 @@ V2版只支持`APK Signature Scheme v2`,要求在 `signingConfigs` 里 `v2Sign ```groovy buildscript { - ...... dependencies{ classpath 'com.mcxiaoke.packer-ng:plugin:2.0.0' } @@ -44,8 +43,7 @@ dependencies { ### 插件配置示例 -``` -//packer-begin +```groovy packer { archiveNameFormat = '${buildType}-v${versionName}-${channel}' archiveOutput = new File(project.rootProject.buildDir, "apks") @@ -58,7 +56,6 @@ packer { "Fish": project.rootProject.file("channels/channels.txt") ] } -//packer-end ``` * **archiveNameFormat** - 指定最终输出的渠道包文件名的格式模版,详细说明见后面,默认值是 `${appPkg}-${channel}-${buildType}-v${versionName}-${versionCode}` (可选) @@ -81,7 +78,6 @@ Gradle_Test# 这是注释 SomeMarket#some market 中文渠道 # comments HelloWorld - ``` ### 集成打包 @@ -206,7 +202,7 @@ String market = PackerNg.getMarket(Context) #### 联系方式 * Blog: * Github: -* Email: [packer-ng-plugin@mcxiaoke.com](mailto: packer-ng-plugin@mcxiaoke.com) +* Email: [packer-ng-plugin@mcxiaoke.com](mailto:packer-ng-plugin@mcxiaoke.com) #### 开源项目 From e47f418eff6cc16f0cfb929ec6473050e65bd7c7 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Thu, 8 Jun 2017 17:52:58 +0800 Subject: [PATCH 32/67] rename classes, tweak payload write --- .../java/com/mcxiaoke/packer/cli/Bridge.java | 6 +- .../com/mcxiaoke/packer/common/CPacker.java | 47 ---- .../com/mcxiaoke/packer/common/CPayload.java | 123 ---------- .../java/com/mcxiaoke/packer/common/Main.java | 22 ++ .../mcxiaoke/packer/common/PackerCommon.java | 211 ++++++++++++++++++ .../mcxiaoke/packer/common/PayloadTests.java | 45 ++-- gradle.properties | 2 +- .../com/mcxiaoke/packer/helper/PackerNg.java | 4 +- sample/build.gradle | 2 +- 9 files changed, 262 insertions(+), 200 deletions(-) delete mode 100644 common/src/main/java/com/mcxiaoke/packer/common/CPacker.java delete mode 100644 common/src/main/java/com/mcxiaoke/packer/common/CPayload.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/common/Main.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java index e73b3bc..3cdb3da 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java @@ -4,7 +4,7 @@ import com.android.apksig.ApkVerifier.Builder; import com.android.apksig.ApkVerifier.Result; import com.android.apksig.apk.ApkFormatException; -import com.mcxiaoke.packer.common.CPacker; +import com.mcxiaoke.packer.common.PackerCommon; import java.io.File; import java.io.IOException; @@ -18,11 +18,11 @@ public class Bridge { public static void writeChannel(File file, String channel) throws IOException { - CPacker.of(file).writeChannel(channel); + PackerCommon.writeChannel(file, channel); } public static String readChannel(File file) throws IOException { - return CPacker.of(file).readChannel(); + return PackerCommon.readChannel(file); } public static boolean verifyChannel(File file, String channel) throws IOException { diff --git a/common/src/main/java/com/mcxiaoke/packer/common/CPacker.java b/common/src/main/java/com/mcxiaoke/packer/common/CPacker.java deleted file mode 100644 index 7b12360..0000000 --- a/common/src/main/java/com/mcxiaoke/packer/common/CPacker.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.mcxiaoke.packer.common; - -import java.io.File; -import java.io.IOException; - -/** - * User: mcxiaoke - * Date: 2017/5/17 - * Time: 15:39 - */ -public class CPacker { - - - public static CPacker of(File apkFile) { - return new CPacker(apkFile, PLUGIN_CHANNEL_KEY, PLUGIN_BLOCK_ID); - } - - // channel info key - public static final String PLUGIN_CHANNEL_KEY = "zKey"; // 0x7a4b6579 - // channel extra key - public static final String PLUGIN_EXTRA_KEY = "zExt"; // 0x7a457874 - // plugin block id - public static final int PLUGIN_BLOCK_ID = 0x7a786b21; // "zxk!" - - - private File apkFile; - private String key; - private int blockId; - - CPacker(final File apkFile, - final String key, - final int blockId) { - this.apkFile = apkFile; - this.key = key; - this.blockId = blockId; - } - - public String readChannel() throws IOException { - return CPayload.readValue(apkFile, key, blockId); - } - - public void writeChannel(final String channel) throws IOException { - CPayload.writeValue(apkFile, key, channel, blockId); - } - - -} diff --git a/common/src/main/java/com/mcxiaoke/packer/common/CPayload.java b/common/src/main/java/com/mcxiaoke/packer/common/CPayload.java deleted file mode 100644 index efe5135..0000000 --- a/common/src/main/java/com/mcxiaoke/packer/common/CPayload.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.mcxiaoke.packer.common; - -import com.mcxiaoke.packer.support.walle.PayloadReader; -import com.mcxiaoke.packer.support.walle.PayloadWriter; - -import java.io.File; -import java.io.IOException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; - -/** - * User: mcxiaoke - * Date: 2017/5/26 - * Time: 13:18 - */ -public class CPayload { - // charset utf8 - private static final String UTF8 = "UTF-8"; - - public static String readValue(File apkFile, - String key, - int blockId) throws IOException { - final Map map = readValues(apkFile, blockId); - if (map == null || map.isEmpty()) { - return null; - } - return map.get(key); - } - - public static void writeValue(File apkFile, - String key, - String value, - int blockId) throws IOException { - final Map values = new HashMap<>(); - values.put(key, value); - writeValues(apkFile, values, blockId); - } - - public static Map readValues(File apkFile, int blockId) - throws IOException { - final String content = readString(apkFile, blockId); - return mapFromString(content); - } - - public static String readString(File apkFile, int blockId) throws IOException { - final byte[] bytes = readBytes(apkFile, blockId); - if (bytes == null || bytes.length == 0) { - return null; - } - return new String(bytes, UTF8); - } - - public static byte[] readBytes(File apkFile, int blockId) throws IOException { - return PayloadReader.readBlock(apkFile, blockId); - } - - public static void writeValues(File apkFile, Map values, int blockId) - throws IOException { - if (values == null || values.isEmpty()) { - return; - } - final Map newValues = new HashMap<>(); - final Map oldValues = readValues(apkFile, blockId); - if (oldValues != null) { - newValues.putAll(oldValues); - } - newValues.putAll(values); - writeString(apkFile, mapToString(newValues), blockId); - } - - public static void writeString(File apkFile, final String content, int blockId) - throws IOException { - PayloadWriter.writeBlock(apkFile, blockId, content.getBytes(UTF8)); - } - - public static void writeBytes(File apkFile, final byte[] bytes, int blockId) - throws IOException { - PayloadWriter.writeBlock(apkFile, blockId, bytes); - } - - public static final String SEP_KV = "∘";//\u2218 - public static final String SEP_LINE = "∙";//\u2219 - - private static String mapToString(final Map map) throws IOException { - if (map == null || map.isEmpty()) { - return null; - } - - final StringBuilder builder = new StringBuilder(); - for (Entry entry : map.entrySet()) { - builder.append(entry.getKey()).append(SEP_KV) - .append(entry.getValue()).append(SEP_LINE); - } - return builder.toString(); - } - - private static Map mapFromString(final String string) { - if (string == null || string.length() == 0) { - return null; - } - final Map map = new HashMap<>(); - final String[] entries = string.split(SEP_LINE); - for (String entry : entries) { - final String[] kv = entry.split(SEP_KV); - if (kv.length == 2) { - map.put(kv[0], kv[1]); - } - } - return map; - } - - private static final String DATE_FORMAT = "yyyy/MM/dd HH:mm:ss Z"; - - private static String getDateString() { - final DateFormat df = new SimpleDateFormat(DATE_FORMAT, Locale.US); - return df.format(new Date()); - } -} diff --git a/common/src/main/java/com/mcxiaoke/packer/common/Main.java b/common/src/main/java/com/mcxiaoke/packer/common/Main.java new file mode 100644 index 0000000..6468f3b --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/common/Main.java @@ -0,0 +1,22 @@ +package com.mcxiaoke.packer.common; + +import java.io.UnsupportedEncodingException; + +/** + * User: mcxiaoke + * Date: 2017/6/8 + * Time: 16:21 + */ + +class Main { + public static void main(String[] args) throws UnsupportedEncodingException { + System.out.println("magic string length=" + + PackerCommon.BLOCK_MAGIC.length()); + System.out.println("magic bytes length=" + + PackerCommon.BLOCK_MAGIC.getBytes(PackerCommon.UTF8).length); + System.out.println("channel key string length=" + + PackerCommon.CHANNEL_KEY.length()); + System.out.println("channel key bytes length=" + + PackerCommon.CHANNEL_KEY.getBytes(PackerCommon.UTF8).length); + } +} diff --git a/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java new file mode 100644 index 0000000..79d5976 --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java @@ -0,0 +1,211 @@ +package com.mcxiaoke.packer.common; + +import com.mcxiaoke.packer.support.walle.PayloadReader; +import com.mcxiaoke.packer.support.walle.PayloadWriter; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; + +/** + * User: mcxiaoke + * Date: 2017/5/26 + * Time: 13:18 + */ +public class PackerCommon { + public static final String SEP_KV = "∘";//\u2218 + public static final String SEP_LINE = "∙";//\u2219 + // charset utf8 + public static final String UTF8 = "UTF-8"; + // date string format + private static final String DATE_FORMAT = "yyyy/MM/dd HH:mm:ss Z"; + // plugin block magic + public static final String BLOCK_MAGIC = "Packer Ng Sig V2"; // magic + + // channel block id + public static final int CHANNEL_BLOCK_ID = 0x7a786b21; // "zxk!" + // channel info key + public static final String CHANNEL_KEY = "CHANNEL"; + + public static String readChannel(File file) throws IOException { + return readValue(file, CHANNEL_KEY, CHANNEL_BLOCK_ID); + } + + public static void writeChannel(File file, String channel) + throws IOException { + writeValue(file, CHANNEL_KEY, channel, CHANNEL_BLOCK_ID); + } + + // package visible for test + static String readValue(File file, + String key, + int blockId) + throws IOException { + final Map map = readValues(file, blockId); + if (map == null || map.isEmpty()) { + return null; + } + return map.get(key); + } + + // package visible for test + static void writeValue(File file, + String key, + String value, + int blockId) + throws IOException { + final Map values = new HashMap<>(); + values.put(key, value); + writeValues(file, values, blockId); + } + + public static Map readValues(File file, int blockId) + throws IOException { + final String content = readString(file, blockId); + return mapFromString(content); + } + + public static String readString(File file, int blockId) + throws IOException { + final byte[] bytes = readBytes(file, blockId); + if (bytes == null || bytes.length == 0) { + return null; + } + return new String(bytes, UTF8); + } + + public static byte[] readBytes(File file, int blockId) + throws IOException { + return readPayloadImpl(file, blockId); + } + + public static void writeValues(File file, + Map values, + int blockId) + throws IOException { + if (values == null || values.isEmpty()) { + return; + } + final Map newValues = new HashMap<>(); + final Map oldValues = readValues(file, blockId); + if (oldValues != null) { + newValues.putAll(oldValues); + } + newValues.putAll(values); + writeString(file, mapToString(newValues), blockId); + } + + public static void writeString(File file, + String content, + int blockId) + throws IOException { + writeBytes(file, content.getBytes(UTF8), blockId); + } + + public static void writeBytes(File file, + byte[] payload, + int blockId) + throws IOException { + writePayloadImpl(file, payload, blockId); + } + + // package visible for test + static void writePayloadImpl(File file, + byte[] payload, + int blockId) + throws IOException { + ByteBuffer buffer = wrapPayload(payload); + PayloadWriter.writeBlock(file, blockId, buffer); + } + + // package visible for test + static byte[] readPayloadImpl(File file, int blockId) + throws IOException { + ByteBuffer buffer = PayloadReader.readBlockBuffer(file, blockId); + if (buffer == null) { + return null; + } + byte[] expected = BLOCK_MAGIC.getBytes(UTF8); + byte[] actual = new byte[expected.length]; + buffer.get(actual); + if (Arrays.equals(expected, actual)) { + int payloadLen = buffer.getInt(); + byte[] payload = new byte[payloadLen]; + buffer.get(payload); + return payload; + } + return null; + } + + // package visible for test + static ByteBuffer wrapPayload(byte[] payload) + throws UnsupportedEncodingException { + /* + + PLUGIN BLOCK LAYOUT + OFFSET DATA TYPE DESCRIPTION + @+0 magic string magic string 16 bytes + @+16 payload length payload length int 4 bytes + @+20 payload payload data bytes + @-20 payload length same as @+16 4 bytes + @-16 magic string same as @+0 16 bytes + + */ + byte[] magic = BLOCK_MAGIC.getBytes(UTF8); + int magicLen = magic.length; + int payloadLen = payload.length; + int length = (magicLen + 4) * 2 + payloadLen; + ByteBuffer buffer = ByteBuffer.allocate(length); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put(magic); //16 + buffer.putInt(payloadLen); //4 payload length + buffer.put(payload); // payload + buffer.putInt(payloadLen); // 4 + buffer.put(magic); //16 + buffer.flip(); + return buffer; + } + + // package visible for test + static String mapToString(Map map) + throws IOException { + final StringBuilder builder = new StringBuilder(); + for (Entry entry : map.entrySet()) { + builder.append(entry.getKey()).append(SEP_KV) + .append(entry.getValue()).append(SEP_LINE); + } + return builder.toString(); + } + + // package visible for test + static Map mapFromString(final String string) { + if (string == null || string.length() == 0) { + return null; + } + final Map map = new HashMap<>(); + final String[] entries = string.split(SEP_LINE); + for (String entry : entries) { + final String[] kv = entry.split(SEP_KV); + if (kv.length == 2) { + map.put(kv[0], kv[1]); + } + } + return map; + } + + // package visible for test + static String getDateString() { + final DateFormat df = new SimpleDateFormat(DATE_FORMAT, Locale.US); + return df.format(new Date()); + } +} diff --git a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java index d989d6d..de7789a 100644 --- a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java +++ b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java @@ -77,8 +77,8 @@ public void testOverrideSignature() throws IOException, NoSuchAlgorithmException { File f = newTestFile(); // don't write with APK Signature Scheme v2 Block ID 0x7109871a - CPayload.writeString(f, "OverrideSignatureSchemeBlock", 0x7109871a); - assertEquals("OverrideSignatureSchemeBlock", CPayload.readString(f, 0x7109871a)); + PackerCommon.writeString(f, "OverrideSignatureSchemeBlock", 0x7109871a); + assertEquals("OverrideSignatureSchemeBlock", PackerCommon.readString(f, 0x7109871a)); ApkVerifier verifier = new Builder(f).build(); Result result = verifier.verify(); final List errors = result.getErrors(); @@ -113,10 +113,10 @@ public void testBytesWrite2() throws IOException { public void testStringWrite() throws IOException { File f = newTestFile(); - CPayload.writeString(f, "Test String", 0x717a786b); - assertEquals("Test String", CPayload.readString(f, 0x717a786b)); - CPayload.writeString(f, "中文和特殊符号测试!@#¥%……*()《》?:【】、", 0x717a786b); - assertEquals("中文和特殊符号测试!@#¥%……*()《》?:【】、", CPayload.readString(f, 0x717a786b)); + PackerCommon.writeString(f, "Test String", 0x717a786b); + assertEquals("Test String", PackerCommon.readString(f, 0x717a786b)); + PackerCommon.writeString(f, "中文和特殊符号测试!@#¥%……*()《》?:【】、", 0x717a786b); + assertEquals("中文和特殊符号测试!@#¥%……*()《》?:【】、", PackerCommon.readString(f, 0x717a786b)); checkApkVerified(f); } @@ -127,8 +127,8 @@ public void testValuesWrite() throws IOException { in.put("名字", "哈哈啊哈哈哈"); in.put("!@#$!%^@&*()_+\"?:><", "渠道Google"); in.put("12345abcd", "2017"); - CPayload.writeValues(f, in, 0x12345); - Map out = CPayload.readValues(f, 0x12345); + PackerCommon.writeValues(f, in, 0x12345); + Map out = PackerCommon.readValues(f, 0x12345); assertNotNull(out); assertEquals(in.size(), out.size()); for (Map.Entry entry : in.entrySet()) { @@ -142,19 +142,19 @@ public void testValuesMixedWrite() throws IOException { Map in = new HashMap<>(); in.put("!@#$!%^@&*()_+\"?:><", "渠道Google"); in.put("12345abcd", "2017"); - CPayload.writeValues(f, in, 0x123456); - CPayload.writeValue(f, "Mixed", "hello", 0x8888); - Map out = CPayload.readValues(f, 0x123456); + PackerCommon.writeValues(f, in, 0x123456); + PackerCommon.writeValue(f, "hello", "Mixed", 0x8888); + Map out = PackerCommon.readValues(f, 0x123456); assertNotNull(out); assertEquals(in.size(), out.size()); for (Map.Entry entry : in.entrySet()) { assertEquals(entry.getValue(), out.get(entry.getKey())); } - assertEquals("Mixed", CPayload.readValue(f, "hello", 0x8888)); - CPayload.writeString(f, "RawValue", 0x2017); - assertEquals("RawValue", CPayload.readString(f, 0x2017)); - CPayload.writeString(f, "OverrideValues", 0x123456); - assertEquals("OverrideValues", CPayload.readString(f, 0x123456)); + assertEquals("Mixed", PackerCommon.readValue(f, "hello", 0x8888)); + PackerCommon.writeString(f, "RawValue", 0x2017); + assertEquals("RawValue", PackerCommon.readString(f, 0x2017)); + PackerCommon.writeString(f, "OverrideValues", 0x123456); + assertEquals("OverrideValues", PackerCommon.readString(f, 0x123456)); checkApkVerified(f); } @@ -218,13 +218,12 @@ public void testBufferWrite() throws IOException { public void testChannelWriteRead() throws IOException { File f = newTestFile(); - CPacker p = CPacker.of(f); - p.writeChannel("Hello"); - assertEquals("Hello", p.readChannel()); - p.writeChannel("中文"); - assertEquals("中文", p.readChannel()); - p.writeChannel("中文 C"); - assertEquals("中文 C", p.readChannel()); + PackerCommon.writeChannel(f, "Hello"); + assertEquals("Hello", PackerCommon.readChannel(f)); + PackerCommon.writeChannel(f, "中文"); + assertEquals("中文", PackerCommon.readChannel(f)); + PackerCommon.writeChannel(f, "中文 C"); + assertEquals("中文 C", PackerCommon.readChannel(f)); checkApkVerified(f); } diff --git a/gradle.properties b/gradle.properties index 0df7616..6b05a63 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.8.0 +VERSION_NAME=1.8.4-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java index 79e9b17..adba9b1 100644 --- a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java +++ b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java @@ -2,7 +2,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; -import com.mcxiaoke.packer.common.CPacker; +import com.mcxiaoke.packer.common.PackerCommon; import java.io.File; @@ -44,7 +44,7 @@ private static ChannelInfo getMarketInternal(final Context context, try { final ApplicationInfo info = context.getApplicationInfo(); final File apkFile = new File(info.sourceDir); - market = CPacker.of(apkFile).readChannel(); + market = PackerCommon.readChannel(apkFile); } catch (Exception e) { error = e; } diff --git a/sample/build.gradle b/sample/build.gradle index d183329..68ea2ec 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.8.0-SNAPSHOT' + ext.packer_version = '1.8.4-SNAPSHOT' repositories { maven { url '/tmp/repo/' } From 4f84ada4e60e8bf3974b5ce85ae1f69a0059be98 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Thu, 8 Jun 2017 17:53:14 +0800 Subject: [PATCH 33/67] more find functions to python script --- tools/packer-ng-v2.py | 422 ++++++++++++++++++++++++++++-------------- 1 file changed, 284 insertions(+), 138 deletions(-) diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index 3d514e1..b4cb4b1 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -2,48 +2,21 @@ # @Author: mcxiaoke # @Date: 2017-06-06 14:03:18 # @Last Modified by: mcxiaoke -# @Last Modified time: 2017-06-07 15:55:31 +# @Last Modified time: 2017-06-08 17:52:03 from __future__ import print_function -# from __future__ import unicode_literals import os import sys import mmap import struct import zipfile import logging +import time -logging.basicConfig(format='%(levelname)s:%(lineno)s:%(funcName)s() %(message)s', level=logging.ERROR) +logging.basicConfig(format='%(levelname)s:%(lineno)s: %(funcName)s() %(message)s', + level=logging.ERROR) logger = logging.getLogger(__name__) -# ref: https://android.googlesource.com/platform/tools/apksig/+/master -# ref: https://source.android.com/security/apksigning/v2 - -ZIP_EOCD_REC_MIN_SIZE = 22 -ZIP_EOCD_REC_SIG = 0x06054b50 -ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10 -ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12 -ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16 -ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20 -ZIP_EOCD_COMMENT_MIN_LENGTH = 0 - -UINT16_MAX_VALUE = 0xffff # 65535 - -APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42 -APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041 -APK_SIG_BLOCK_MIN_SIZE = 32 -APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a - -# channel info key -PLUGIN_CHANNEL_KEY = 'zKey' # 0x7a4b6579 -# channel extra key -PLUGIN_EXTRA_KEY = 'zExt' # 0x7a457874 -# channel date key -PLUGIN_DATE_KEY = 'zDat' # 0x7a446174 -# plugin block id -PLUGIN_BLOCK_ID = 0x7a786b21 # "zxk!" - -SEP_KV = '∘' -SEP_LINE = '∙' +##################################################################### AUTHOR = 'mcxiaoke' VERSION = '1.0.0' @@ -53,104 +26,143 @@ except Exception as e: VERSION = '1.0.0' -logger.debug('AUTHOR:%s', AUTHOR) -logger.debug('VERSION:%s', VERSION) - - -class ZipFormatException(Exception): - pass - - -class SignatureNotFoundException(Exception): - pass - -class ByteDecoder(object): - ''' - byte array decoder - https://docs.python.org/2/library/struct.html - ''' - - def __init__(self, buf, littleEndian=True): - self.buf = buf - self.sign = '<' if littleEndian else '>' - - def getShort(self, offset=0): - return struct.unpack('{}h'.format(self.sign), self.buf[offset:offset+2])[0] - - def getUShort(self, offset=0): - return struct.unpack('{}H'.format(self.sign), self.buf[offset:offset+2])[0] - - def getInt(self, offset=0): - return struct.unpack('{}i'.format(self.sign), self.buf[offset:offset+4])[0] - - def getUInt(self, offset=0): - return struct.unpack('{}I'.format(self.sign), self.buf[offset:offset+4])[0] - - def getLong(self, offset=0): - return struct.unpack('{}q'.format(self.sign), self.buf[offset:offset+8])[0] - - def getULong(self, offset=0): - return struct.unpack('{}Q'.format(self.sign), self.buf[offset:offset+8])[0] - - def getFloat(self, offset=0): - return struct.unpack('{}f'.format(self.sign), self.buf[offset:offset+4])[0] - - def getDouble(self, offset=0): - return struct.unpack('{}d'.format(self.sign), self.buf[offset:offset+8])[0] - - def getChars(self, offset=0, size=16): - return struct.unpack('{}{}'.format(self.sign, 's'*size), self.buf[offset:offset+size]) - - -class ZipSections(object): - ''' - long centralDirectoryOffset, - long centralDirectorySizeBytes, - int centralDirectoryRecordCount, - long eocdOffset, - ByteBuffer eocd +def main(): + logger.debug('AUTHOR:%s', AUTHOR) + logger.debug('VERSION:%s', VERSION) + prog = os.path.basename(sys.argv[0]) + if len(sys.argv) < 2: + print('Usage: {} app.apk'.format(prog)) + sys.exit(1) + apk = os.path.abspath(sys.argv[1]) + from apkinfo import APK + info = APK(apk) + try: + print('File: \t\t{}'.format(os.path.basename(apk))) + print('Package: \t{}'.format(info.get_package())) + print('Version: \t{}'.format(info.get_version_name())) + print('Build: \t\t{}'.format(info.get_version_code())) + channel = getChannel(apk) + print('Channel: \t{}'.format(channel)) + # test(apk) + except Exception as e: + print("Error:", e) - ''' +if __name__ == '__main__': + main() - def __init__(self, cdStartOffset, - cdSizeBytes, - cdRecordCount, - eocdOffset, - eocd): - self.cdStartOffset = cdStartOffset - self.cdSizeBytes = cdSizeBytes - self.cdRecordCount = cdRecordCount - self.eocdOffset = eocdOffset - self.eocd = eocd +##################################################################### def getChannel(apk): apk = os.path.abspath(apk) - logger.debug('apk:%s', apk) - values = findPluginBlockValues(apk) + logger.debug('apk:%s', os.path.basename(apk)) + zp = zipfile.ZipFile(apk) + zp.testzip() + values = findValues(apk) if values: channel = values.get(PLUGIN_CHANNEL_KEY) - extra = values.get(PLUGIN_EXTRA_KEY) logger.debug('channel:%s', channel) - logger.debug('extra:%s', extra) return channel else: logger.debug('channel not found') -def findPluginBlockValues(apk): - apkSigningBlock = findApkSigningBlock(apk) - block = parseApkSigningBlock(apkSigningBlock, PLUGIN_BLOCK_ID) - if block: - values = dict(line.split(SEP_KV) for line in block.split(SEP_LINE) if line.strip()) - logger.debug('values:%s', values) - return values +def findValues(apk): + ''' + PLUGIN BLOCK LAYOUT + OFFSET DATA TYPE DESCRIPTION + @+0 magic string magic string 16 bytes + @+16 payload length payload length int 4 bytes + @+20 payload payload data bytes + @-20 payload length same as @+16 4 bytes + @-16 magic string same as @+0 16 bytes + ''' + content = findBlock1(apk) + magicLen = len(PLUGIN_BLOCK_MAGIC) + logger.debug('content:%s', content) + if not content or len(content) < 2*(magicLen + 4): + return None + content = content[magicLen+4:-(magicLen+4)] + values = dict(line.split(SEP_KV) + for line in content.split(SEP_LINE) if line.strip()) + logger.debug('values:%s', values) + return values -def findApkSigningBlock(apk): - zp = zipfile.ZipFile(apk) - zp.testzip() +def findBlock1(apk): + # # search Plugin Magic words + with open(apk, "rb") as f: + mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) + d = ByteDecoder(mm) + size = mm.size() + logger.debug('file size=%s', size) + magicLen = len(PLUGIN_BLOCK_MAGIC) + end = mm.rfind(PLUGIN_BLOCK_MAGIC) + if end == -1: + raise MagicNotFoundException( + 'Plugin Magic words not found') + logger.debug('magic end offset=%s', end) + magic = ''.join(d.getChars(end, magicLen)) + logger.debug('magic end string=%s', magic) + payloadLen = d.getInt(end-4) + logger.debug('magic payloadLen1=%s', payloadLen) + + start = end - payloadLen - 8 - magicLen + if start == -1: + raise MagicNotFoundException( + 'Plugin Magic words not found') + logger.debug('magic start offset=%s', start) + logger.debug('magic start string=%s', ''.join(d.getChars(start, magicLen))) + logger.debug('magic payloadLen2=%s', d.getInt(start+magicLen)) + + block = mm[start:end+magicLen] + mm.close() + return block + + +def findBlock2(apk): + # search APK Signing Block Magic words + signingBlock = findBySigningMagic(apk) + if signingBlock: + return parseApkSigningBlock(signingBlock, PLUGIN_BLOCK_ID) + + +def findBlock3(apk): + # find zip centralDirectory, then find apkSigningBlock + signingBlock = findByZipSections(apk) + if signingBlock: + return parseApkSigningBlock(signingBlock, PLUGIN_BLOCK_ID) + + +def findBySigningMagic(apk): + # findApkSigningBlockUsingSigningMagic + with open(apk, "rb") as f: + mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) + d = ByteDecoder(mm) + size = mm.size() + logger.debug('file size=%s', size) + offset = size - UINT16_MAX_VALUE + logger.debug('file offset=%s', offset) + index = mm.find(APK_SIG_BLOCK_MAGIC, offset) + if index == -1: + raise MagicNotFoundException( + 'APK Signing Block Magic not found') + logger.debug('magic index=%s', index) + logger.debug('magic string=%s', ''.join(d.getChars(index, 16))) + bEnd = index + 16 + logger.debug('block end=%s', bEnd) + bSize = d.getLong(bEnd-24)+8 + logger.debug('block size=%s', bSize) + bStart = bEnd - bSize + logger.debug('block start=%s', bStart) + block = mm[bStart:bEnd] + mm.close() + return block + + +def findByZipSections(apk): + # findApkSigningBlockUsingZipSections with open(apk, "rb") as f: mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) sections = findZipSections(mm) @@ -172,19 +184,28 @@ def findApkSigningBlock(apk): + centralDirStartOffset) fStart = centralDirStartOffset-24 + mStart = centralDirStartOffset - 16 fEnd = centralDirStartOffset + logger.debug('fStart:%s', fStart) + logger.debug('mStart:%s', mStart) + logger.debug('fEnd:%s', fEnd) footer = mm[fStart:fEnd] footerSize = len(footer) # logger.debug('footer:%s',to_hex(footer)) fd = ByteDecoder(footer) + magic = ''.join(fd.getChars(8, 16)) + logger.debug('magic str:%s', magic) lo = fd.getLong(8) hi = fd.getLong(16) logger.debug('magic lo:%s', hex(lo)) logger.debug('magic hi:%s', hex(hi)) - if lo != APK_SIG_BLOCK_MAGIC_LO or hi != APK_SIG_BLOCK_MAGIC_HI: + if magic != APK_SIG_BLOCK_MAGIC: raise SignatureNotFoundException( "No APK Signing Block before ZIP Central Directory") + # if lo != APK_SIG_BLOCK_MAGIC_LO or hi != APK_SIG_BLOCK_MAGIC_HI: + # raise SignatureNotFoundException( + # "No APK Signing Block before ZIP Central Directory") apkSigBlockSizeInFooter = fd.getLong(0) logger.debug('apkSigBlockSizeInFooter:%s', apkSigBlockSizeInFooter) @@ -213,11 +234,15 @@ def findApkSigningBlock(apk): "APK Signing Block sizes in header and footer do not match: " + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter) - apkSigningBlock = mm[apkSigBlockOffset:apkSigBlockOffset+totalSize] - return apkSigningBlock + block = mm[apkSigBlockOffset:apkSigBlockOffset+totalSize] + mm.close() + return block def parseApkSigningBlock(block, blockId): + # parseApkSigningBlock + if not block or not blockId: + return None ''' // APK Signing Block // FORMAT: @@ -227,10 +252,17 @@ def parseApkSigningBlock(block, blockId): // * @-24 bytes uint64: size in bytes(same as the one above) // * @-16 bytes uint128: magic ''' - block = block[8:-24] # only payload + totalSize = len(block) + bd0 = ByteDecoder(block) + blockSizeInHeader = bd0.getULong(0) + logger.debug('blockSizeInHeader:%s', blockSizeInHeader) + blockSizeInFooter = bd0.getULong(totalSize-24) + logger.debug('blockSizeInFooter:%s', blockSizeInFooter) + # slice only payload + block = block[8:-24] bd = ByteDecoder(block) size = len(block) - logger.debug('size:%s', size) + logger.debug('payloadSize:%s', size) entryCount = 0 position = 0 @@ -238,10 +270,10 @@ def parseApkSigningBlock(block, blockId): channelBlock = None while position < size: entryCount += 1 - logger.debug('----------') logger.debug('entryCount:%s', entryCount) if size - position < 8: - raise SignatureNotFoundException('Insufficient data to read size of APK Signing Block entry: {}'.format(entryCount)) + raise SignatureNotFoundException( + 'Insufficient data to read size of APK Signing Block entry: {}'.format(entryCount)) lenLong = bd.getLong(position) logger.debug('lenLong:%s', lenLong) position += 8 @@ -280,7 +312,8 @@ def parseApkSigningBlock(block, blockId): def findZipSections(mm): eocd = findEocdRecord(mm) if not eocd: - raise ZipFormatException("ZIP End of Central Directory record not found") + raise ZipFormatException( + "ZIP End of Central Directory record not found") eocdOffset, eocdBuf = eocd ed = ByteDecoder(eocdBuf) # logger.debug('eocdBuf:%s', to_hex(eocdBuf)) @@ -355,25 +388,138 @@ def findEocdStartOffset(buf): expectedCommentLength += 1 return -1 +##################################################################### + + +@timeit +def test(apk): + for i in range(500): + channel = getChannel(apk) + def to_hex(s): return " ".join("{:02x}".format(ord(c)) for c in s) if s else "" -if __name__ == '__main__': - prog = os.path.basename(sys.argv[0]) - if len(sys.argv) < 2: - print('Usage: {} app.apk'.format(prog)) - sys.exit(1) - apk = os.path.abspath(sys.argv[1]) - from apkinfo import APK - info = APK(apk) - try: - print('File: \t\t{}'.format(os.path.basename(apk))) - print('Package: \t{}'.format(info.get_package())) - print('Version: \t{}'.format(info.get_version_name())) - print('Build: \t\t{}'.format(info.get_version_code())) - channel = getChannel(apk) - print('Channel: \t{}'.format(channel)) - except Exception as e: - print("Error:", e) +def timeit(method): + + def timed(*args, **kw): + ts = time.time() * 1000 + result = method(*args, **kw) + te = time.time() * 1000 + + print('%s() executed in %.2f msec' % (method.__name__, te - ts)) + return result + + return timed + +##################################################################### + + +class ZipFormatException(Exception): + pass + + +class SignatureNotFoundException(Exception): + pass + + +class MagicNotFoundException(Exception): + pass + +##################################################################### + + +class ByteDecoder(object): + ''' + byte array decoder + https://docs.python.org/2/library/struct.html + ''' + + def __init__(self, buf, littleEndian=True): + self.buf = buf + self.sign = '<' if littleEndian else '>' + + def getShort(self, offset=0): + return struct.unpack('{}h'.format(self.sign), self.buf[offset:offset+2])[0] + + def getUShort(self, offset=0): + return struct.unpack('{}H'.format(self.sign), self.buf[offset:offset+2])[0] + + def getInt(self, offset=0): + return struct.unpack('{}i'.format(self.sign), self.buf[offset:offset+4])[0] + + def getUInt(self, offset=0): + return struct.unpack('{}I'.format(self.sign), self.buf[offset:offset+4])[0] + + def getLong(self, offset=0): + return struct.unpack('{}q'.format(self.sign), self.buf[offset:offset+8])[0] + + def getULong(self, offset=0): + return struct.unpack('{}Q'.format(self.sign), self.buf[offset:offset+8])[0] + + def getFloat(self, offset=0): + return struct.unpack('{}f'.format(self.sign), self.buf[offset:offset+4])[0] + + def getDouble(self, offset=0): + return struct.unpack('{}d'.format(self.sign), self.buf[offset:offset+8])[0] + + def getChars(self, offset=0, size=16): + return struct.unpack('{}{}'.format(self.sign, 's'*size), self.buf[offset:offset+size]) + +##################################################################### + + +class ZipSections(object): + ''' + long centralDirectoryOffset, + long centralDirectorySizeBytes, + int centralDirectoryRecordCount, + long eocdOffset, + ByteBuffer eocd + ''' + + def __init__(self, cdStartOffset, + cdSizeBytes, + cdRecordCount, + eocdOffset, + eocd): + self.cdStartOffset = cdStartOffset + self.cdSizeBytes = cdSizeBytes + self.cdRecordCount = cdRecordCount + self.eocdOffset = eocdOffset + self.eocd = eocd + +##################################################################### + + +# ref: https://android.googlesource.com/platform/tools/apksig/+/master +# ref: https://source.android.com/security/apksigning/v2 + +ZIP_EOCD_REC_MIN_SIZE = 22 +ZIP_EOCD_REC_SIG = 0x06054b50 +ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10 +ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12 +ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16 +ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20 +ZIP_EOCD_COMMENT_MIN_LENGTH = 0 + +UINT16_MAX_VALUE = 0xffff # 65535 + +APK_SIG_BLOCK_MAGIC = 'APK Sig Block 42' +APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42 +APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041 +APK_SIG_BLOCK_MIN_SIZE = 32 +APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a + +# plugin channel key +PLUGIN_CHANNEL_KEY = 'CHANNEL' +# plugin block id +PLUGIN_BLOCK_ID = 0x7a786b21 +# plugin block magic +PLUGIN_BLOCK_MAGIC = 'Packer Ng Sig V2' + +SEP_KV = '∘' +SEP_LINE = '∙' + +##################################################################### From 81d47bb62f1d65de68c98bd49d836a2875e368fa Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 11:51:14 +0800 Subject: [PATCH 34/67] update payload format --- .../mcxiaoke/packer/common/PackerCommon.java | 25 ++++++++++--------- .../mcxiaoke/packer/common/PayloadTests.java | 6 ++--- gradle.properties | 2 +- sample/build.gradle | 2 +- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java index 79d5976..5923fbb 100644 --- a/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java +++ b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java @@ -135,14 +135,19 @@ static byte[] readPayloadImpl(File file, int blockId) if (buffer == null) { return null; } - byte[] expected = BLOCK_MAGIC.getBytes(UTF8); - byte[] actual = new byte[expected.length]; + byte[] magic = BLOCK_MAGIC.getBytes(UTF8); + byte[] actual = new byte[magic.length]; buffer.get(actual); - if (Arrays.equals(expected, actual)) { - int payloadLen = buffer.getInt(); - byte[] payload = new byte[payloadLen]; - buffer.get(payload); - return payload; + if (Arrays.equals(magic, actual)) { + int payloadLength1 = buffer.getInt(); + if (payloadLength1 > 0) { + byte[] payload = new byte[payloadLength1]; + buffer.get(payload); + int payloadLength2 = buffer.getInt(); + if (payloadLength2 == payloadLength1) { + return payload; + } + } } return null; } @@ -151,15 +156,12 @@ static byte[] readPayloadImpl(File file, int blockId) static ByteBuffer wrapPayload(byte[] payload) throws UnsupportedEncodingException { /* - PLUGIN BLOCK LAYOUT OFFSET DATA TYPE DESCRIPTION @+0 magic string magic string 16 bytes @+16 payload length payload length int 4 bytes @+20 payload payload data bytes - @-20 payload length same as @+16 4 bytes - @-16 magic string same as @+0 16 bytes - + @-4 payload length same as @+16 4 bytes */ byte[] magic = BLOCK_MAGIC.getBytes(UTF8); int magicLen = magic.length; @@ -171,7 +173,6 @@ static ByteBuffer wrapPayload(byte[] payload) buffer.putInt(payloadLen); //4 payload length buffer.put(payload); // payload buffer.putInt(payloadLen); // 4 - buffer.put(magic); //16 buffer.flip(); return buffer; } diff --git a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java index de7789a..9fc5b9a 100644 --- a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java +++ b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java @@ -171,7 +171,7 @@ public void testByteBuffer() throws IOException { buf.putDouble(3.14159265); buf.put((byte) 5); buf.flip(); // important - TestUtils.showBuffer(buf); +// TestUtils.showBuffer(buf); assertEquals(123, buf.getInt()); assertEquals('z', buf.getChar()); assertEquals(2017, buf.getShort()); @@ -198,11 +198,11 @@ public void testBufferWrite() throws IOException { in.put((byte) 5); in.put(string); in.flip(); // important - TestUtils.showBuffer(in); +// TestUtils.showBuffer(in); PayloadWriter.writeBlock(f, 0x123456, in); ByteBuffer out = PayloadReader.readBlockBuffer(f, 0x123456); assertNotNull(out); - TestUtils.showBuffer(out); +// TestUtils.showBuffer(out); assertEquals(123, out.getInt()); assertEquals('z', out.getChar()); assertEquals(2017, out.getShort()); diff --git a/gradle.properties b/gradle.properties index 6b05a63..08ea2f7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.8.4-SNAPSHOT +VERSION_NAME=1.8.8-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/sample/build.gradle b/sample/build.gradle index 68ea2ec..46e976a 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.8.4-SNAPSHOT' + ext.packer_version = '1.8.8-SNAPSHOT' repositories { maven { url '/tmp/repo/' } From faed97bc49ae10480c501e03e6badd1ed41532e3 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 11:51:40 +0800 Subject: [PATCH 35/67] update python script --- tools/packer-ng-v2.py | 330 +++++++++++++++++++++--------------------- 1 file changed, 162 insertions(+), 168 deletions(-) diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index b4cb4b1..a218b64 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -2,7 +2,7 @@ # @Author: mcxiaoke # @Date: 2017-06-06 14:03:18 # @Last Modified by: mcxiaoke -# @Last Modified time: 2017-06-08 17:52:03 +# @Last Modified time: 2017-06-09 11:41:59 from __future__ import print_function import os import sys @@ -16,8 +16,6 @@ level=logging.ERROR) logger = logging.getLogger(__name__) -##################################################################### - AUTHOR = 'mcxiaoke' VERSION = '1.0.0' try: @@ -26,30 +24,114 @@ except Exception as e: VERSION = '1.0.0' +##################################################################### -def main(): - logger.debug('AUTHOR:%s', AUTHOR) - logger.debug('VERSION:%s', VERSION) - prog = os.path.basename(sys.argv[0]) - if len(sys.argv) < 2: - print('Usage: {} app.apk'.format(prog)) - sys.exit(1) - apk = os.path.abspath(sys.argv[1]) - from apkinfo import APK - info = APK(apk) - try: - print('File: \t\t{}'.format(os.path.basename(apk))) - print('Package: \t{}'.format(info.get_package())) - print('Version: \t{}'.format(info.get_version_name())) - print('Build: \t\t{}'.format(info.get_version_code())) - channel = getChannel(apk) - print('Channel: \t{}'.format(channel)) - # test(apk) - except Exception as e: - print("Error:", e) -if __name__ == '__main__': - main() +# ref: https://android.googlesource.com/platform/tools/apksig/+/master +# ref: https://source.android.com/security/apksigning/v2 + +ZIP_EOCD_REC_MIN_SIZE = 22 +ZIP_EOCD_REC_SIG = 0x06054b50 +ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10 +ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12 +ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16 +ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20 +ZIP_EOCD_COMMENT_MIN_LENGTH = 0 + +UINT16_MAX_VALUE = 0xffff # 65535 + +APK_SIG_BLOCK_MAGIC = 'APK Sig Block 42' +APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42 +APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041 +APK_SIG_BLOCK_MIN_SIZE = 32 +APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a + +# plugin channel key +PLUGIN_CHANNEL_KEY = 'CHANNEL' +# plugin block id +PLUGIN_BLOCK_ID = 0x7a786b21 +# plugin block magic +PLUGIN_BLOCK_MAGIC = 'Packer Ng Sig V2' + +SEP_KV = '∘' +SEP_LINE = '∙' + +##################################################################### + + +class ZipFormatException(Exception): + pass + + +class SignatureNotFoundException(Exception): + pass + + +class MagicNotFoundException(Exception): + pass + +##################################################################### + + +class ByteDecoder(object): + ''' + byte array decoder + https://docs.python.org/2/library/struct.html + ''' + + def __init__(self, buf, littleEndian=True): + self.buf = buf + self.sign = '<' if littleEndian else '>' + + def getShort(self, offset=0): + return struct.unpack('{}h'.format(self.sign), self.buf[offset:offset+2])[0] + + def getUShort(self, offset=0): + return struct.unpack('{}H'.format(self.sign), self.buf[offset:offset+2])[0] + + def getInt(self, offset=0): + return struct.unpack('{}i'.format(self.sign), self.buf[offset:offset+4])[0] + + def getUInt(self, offset=0): + return struct.unpack('{}I'.format(self.sign), self.buf[offset:offset+4])[0] + + def getLong(self, offset=0): + return struct.unpack('{}q'.format(self.sign), self.buf[offset:offset+8])[0] + + def getULong(self, offset=0): + return struct.unpack('{}Q'.format(self.sign), self.buf[offset:offset+8])[0] + + def getFloat(self, offset=0): + return struct.unpack('{}f'.format(self.sign), self.buf[offset:offset+4])[0] + + def getDouble(self, offset=0): + return struct.unpack('{}d'.format(self.sign), self.buf[offset:offset+8])[0] + + def getChars(self, offset=0, size=16): + return struct.unpack('{}{}'.format(self.sign, 's'*size), self.buf[offset:offset+size]) + +##################################################################### + + +class ZipSections(object): + ''' + long centralDirectoryOffset, + long centralDirectorySizeBytes, + int centralDirectoryRecordCount, + long eocdOffset, + ByteBuffer eocd + ''' + + def __init__(self, cdStartOffset, + cdSizeBytes, + cdRecordCount, + eocdOffset, + eocd): + self.cdStartOffset = cdStartOffset + self.cdSizeBytes = cdSizeBytes + self.cdRecordCount = cdRecordCount + self.eocdOffset = eocdOffset + self.eocd = eocd ##################################################################### @@ -57,15 +139,18 @@ def main(): def getChannel(apk): apk = os.path.abspath(apk) logger.debug('apk:%s', os.path.basename(apk)) - zp = zipfile.ZipFile(apk) - zp.testzip() - values = findValues(apk) - if values: - channel = values.get(PLUGIN_CHANNEL_KEY) - logger.debug('channel:%s', channel) - return channel - else: - logger.debug('channel not found') + try: + zp = zipfile.ZipFile(apk) + zp.testzip() + values = findValues(apk) + if values: + channel = values.get(PLUGIN_CHANNEL_KEY) + logger.debug('channel:%s', channel) + return channel + else: + logger.debug('channel not found') + except Exception as e: + logger.error('%s: %s', type(e).__name__, e.message) def findValues(apk): @@ -75,15 +160,14 @@ def findValues(apk): @+0 magic string magic string 16 bytes @+16 payload length payload length int 4 bytes @+20 payload payload data bytes - @-20 payload length same as @+16 4 bytes - @-16 magic string same as @+0 16 bytes + @-4 payload length same as @+16 4 bytes ''' content = findBlock1(apk) magicLen = len(PLUGIN_BLOCK_MAGIC) logger.debug('content:%s', content) - if not content or len(content) < 2*(magicLen + 4): + if not content or len(content) < magicLen + 8: return None - content = content[magicLen+4:-(magicLen+4)] + content = content[magicLen+4:-4] values = dict(line.split(SEP_KV) for line in content.split(SEP_LINE) if line.strip()) logger.debug('values:%s', values) @@ -98,25 +182,23 @@ def findBlock1(apk): size = mm.size() logger.debug('file size=%s', size) magicLen = len(PLUGIN_BLOCK_MAGIC) - end = mm.rfind(PLUGIN_BLOCK_MAGIC) - if end == -1: - raise MagicNotFoundException( - 'Plugin Magic words not found') - logger.debug('magic end offset=%s', end) - magic = ''.join(d.getChars(end, magicLen)) - logger.debug('magic end string=%s', magic) - payloadLen = d.getInt(end-4) - logger.debug('magic payloadLen1=%s', payloadLen) - - start = end - payloadLen - 8 - magicLen + start = mm.rfind(PLUGIN_BLOCK_MAGIC) + # if start == -1: + # raise MagicNotFoundException( + # 'Packer Ng Magic words not found') if start == -1: - raise MagicNotFoundException( - 'Plugin Magic words not found') + return None logger.debug('magic start offset=%s', start) - logger.debug('magic start string=%s', ''.join(d.getChars(start, magicLen))) - logger.debug('magic payloadLen2=%s', d.getInt(start+magicLen)) + magic = ''.join(d.getChars(start, magicLen)) + logger.debug('magic start string=%s', magic) + payloadLen = d.getInt(start + magicLen) + logger.debug('magic payloadLen1=%s', payloadLen) + + end = start + magicLen + 4 + payloadLen + 4 + logger.debug('magic end offset=%s', end) + logger.debug('magic payloadLen2=%s', d.getInt(end-4)) - block = mm[start:end+magicLen] + block = mm[start:end] mm.close() return block @@ -388,17 +470,8 @@ def findEocdStartOffset(buf): expectedCommentLength += 1 return -1 -##################################################################### - -@timeit -def test(apk): - for i in range(500): - channel = getChannel(apk) - - -def to_hex(s): - return " ".join("{:02x}".format(ord(c)) for c in s) if s else "" +##################################################################### def timeit(method): @@ -413,113 +486,34 @@ def timed(*args, **kw): return timed -##################################################################### - - -class ZipFormatException(Exception): - pass - - -class SignatureNotFoundException(Exception): - pass - - -class MagicNotFoundException(Exception): - pass - -##################################################################### - - -class ByteDecoder(object): - ''' - byte array decoder - https://docs.python.org/2/library/struct.html - ''' - - def __init__(self, buf, littleEndian=True): - self.buf = buf - self.sign = '<' if littleEndian else '>' - - def getShort(self, offset=0): - return struct.unpack('{}h'.format(self.sign), self.buf[offset:offset+2])[0] - - def getUShort(self, offset=0): - return struct.unpack('{}H'.format(self.sign), self.buf[offset:offset+2])[0] - - def getInt(self, offset=0): - return struct.unpack('{}i'.format(self.sign), self.buf[offset:offset+4])[0] - - def getUInt(self, offset=0): - return struct.unpack('{}I'.format(self.sign), self.buf[offset:offset+4])[0] - - def getLong(self, offset=0): - return struct.unpack('{}q'.format(self.sign), self.buf[offset:offset+8])[0] - - def getULong(self, offset=0): - return struct.unpack('{}Q'.format(self.sign), self.buf[offset:offset+8])[0] - - def getFloat(self, offset=0): - return struct.unpack('{}f'.format(self.sign), self.buf[offset:offset+4])[0] - - def getDouble(self, offset=0): - return struct.unpack('{}d'.format(self.sign), self.buf[offset:offset+8])[0] - - def getChars(self, offset=0, size=16): - return struct.unpack('{}{}'.format(self.sign, 's'*size), self.buf[offset:offset+size]) - -##################################################################### - -class ZipSections(object): - ''' - long centralDirectoryOffset, - long centralDirectorySizeBytes, - int centralDirectoryRecordCount, - long eocdOffset, - ByteBuffer eocd - ''' - - def __init__(self, cdStartOffset, - cdSizeBytes, - cdRecordCount, - eocdOffset, - eocd): - self.cdStartOffset = cdStartOffset - self.cdSizeBytes = cdSizeBytes - self.cdRecordCount = cdRecordCount - self.eocdOffset = eocdOffset - self.eocd = eocd - -##################################################################### - - -# ref: https://android.googlesource.com/platform/tools/apksig/+/master -# ref: https://source.android.com/security/apksigning/v2 - -ZIP_EOCD_REC_MIN_SIZE = 22 -ZIP_EOCD_REC_SIG = 0x06054b50 -ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10 -ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12 -ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16 -ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20 -ZIP_EOCD_COMMENT_MIN_LENGTH = 0 +@timeit +def test(apk): + for i in range(500): + channel = getChannel(apk) -UINT16_MAX_VALUE = 0xffff # 65535 -APK_SIG_BLOCK_MAGIC = 'APK Sig Block 42' -APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42 -APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041 -APK_SIG_BLOCK_MIN_SIZE = 32 -APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a +def to_hex(s): + return " ".join("{:02x}".format(ord(c)) for c in s) if s else "" -# plugin channel key -PLUGIN_CHANNEL_KEY = 'CHANNEL' -# plugin block id -PLUGIN_BLOCK_ID = 0x7a786b21 -# plugin block magic -PLUGIN_BLOCK_MAGIC = 'Packer Ng Sig V2' -SEP_KV = '∘' -SEP_LINE = '∙' +def main(): + logger.debug('AUTHOR:%s', AUTHOR) + logger.debug('VERSION:%s', VERSION) + prog = os.path.basename(sys.argv[0]) + if len(sys.argv) < 2: + print('Usage: {} app.apk'.format(prog)) + sys.exit(1) + apk = os.path.abspath(sys.argv[1]) + from apkinfo import APK + info = APK(apk) + print('File: \t\t{}'.format(os.path.basename(apk))) + print('Package: \t{}'.format(info.get_package())) + print('Version: \t{}'.format(info.get_version_name())) + print('Build: \t\t{}'.format(info.get_version_code())) + channel = getChannel(apk) + print('Channel: \t{}'.format(channel)) + # test(apk) -##################################################################### +if __name__ == '__main__': + main() From 988d7ebfc51707d19b800b358e87fea5609f9df4 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 15:16:40 +0800 Subject: [PATCH 36/67] delete unused main class --- .../java/com/mcxiaoke/packer/common/Main.java | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 common/src/main/java/com/mcxiaoke/packer/common/Main.java diff --git a/common/src/main/java/com/mcxiaoke/packer/common/Main.java b/common/src/main/java/com/mcxiaoke/packer/common/Main.java deleted file mode 100644 index 6468f3b..0000000 --- a/common/src/main/java/com/mcxiaoke/packer/common/Main.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.mcxiaoke.packer.common; - -import java.io.UnsupportedEncodingException; - -/** - * User: mcxiaoke - * Date: 2017/6/8 - * Time: 16:21 - */ - -class Main { - public static void main(String[] args) throws UnsupportedEncodingException { - System.out.println("magic string length=" - + PackerCommon.BLOCK_MAGIC.length()); - System.out.println("magic bytes length=" - + PackerCommon.BLOCK_MAGIC.getBytes(PackerCommon.UTF8).length); - System.out.println("channel key string length=" - + PackerCommon.CHANNEL_KEY.length()); - System.out.println("channel key bytes length=" - + PackerCommon.CHANNEL_KEY.getBytes(PackerCommon.UTF8).length); - } -} From 3d1d85ebb95130db7e11a4ccb8045f15d4d9035a Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 15:16:51 +0800 Subject: [PATCH 37/67] add kmp search class --- .../com/mcxiaoke/packer/common/KMPMatch.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 common/src/main/java/com/mcxiaoke/packer/common/KMPMatch.java diff --git a/common/src/main/java/com/mcxiaoke/packer/common/KMPMatch.java b/common/src/main/java/com/mcxiaoke/packer/common/KMPMatch.java new file mode 100644 index 0000000..f16f6b5 --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/common/KMPMatch.java @@ -0,0 +1,95 @@ +package com.mcxiaoke.packer.common; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * https://store.fmi.uni-sofia.bg/fmi/logic/vboutchkova/sources/KMPMatch.java + * User: mcxiaoke + * Date: 2017/6/9 + * Time: 12:21 + */ + +class KMPMatch { + + private byte[] pattern; + private int[] failure; + + public KMPMatch(byte[] pattern) { + this.pattern = pattern; + computeFailure(); + } + + public int find(InputStream is) throws IOException { + int i = 0; + int j = 0; + int b; + while ((b = is.read()) != -1) { + i++; + while (j > 0 && pattern[j] != b) { + j = failure[j - 1]; + } + if (pattern[j] == b) { + j++; + } + if (j == pattern.length) { + return i; + } + } + return -1; + } + + public int find(ByteBuffer buf) { + int j = 0; + int p = buf.position(); + while (buf.hasRemaining()) { + byte b = buf.get(); + while (j > 0 && pattern[j] != b) { + j = failure[j - 1]; + } + if (pattern[j] == b) { + j++; + } + if (j == pattern.length) { + int q = buf.position() - p; + return q - pattern.length + 1; + } + } + return -1; + } + + public int find(byte[] data) { + int j = 0; + if (data.length == 0) return -1; + if (data.length < pattern.length) return -1; + + for (int i = 0; i < data.length; i++) { + while (j > 0 && pattern[j] != data[i]) { + j = failure[j - 1]; + } + if (pattern[j] == data[i]) { + j++; + } + if (j == pattern.length) { + return i - pattern.length + 1; + } + } + return -1; + } + + private void computeFailure() { + failure = new int[pattern.length]; + int j = 0; + for (int i = 1; i < pattern.length; i++) { + while (j > 0 && pattern[j] != pattern[i]) { + j = failure[j - 1]; + } + if (pattern[j] == pattern[i]) { + j++; + } + failure[i] = j; + } + } + +} From 12279dbcf3568c5655137c7266015e6e8157242f Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 15:17:06 +0800 Subject: [PATCH 38/67] add kmp channel reader and tests --- .../com/mcxiaoke/packer/common/KMPReader.java | 67 +++++++++++++++++++ .../mcxiaoke/packer/common/PackerCommon.java | 12 ++-- .../mcxiaoke/packer/common/PayloadTests.java | 15 ++++- .../com/mcxiaoke/packer/common/TestUtils.java | 8 +-- 4 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java diff --git a/common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java b/common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java new file mode 100644 index 0000000..c53f37e --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java @@ -0,0 +1,67 @@ +package com.mcxiaoke.packer.common; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.util.Map; + +/** + * User: mcxiaoke + * Date: 2017/6/9 + * Time: 14:47 + */ + +class KMPReader { + + public static String readChannel(File file) throws IOException { + String payload = readPayload(file); + Map values = PackerCommon.mapFromString(payload); + return values == null ? null : values.get(PackerCommon.CHANNEL_KEY); + } + + public static String readPayload(File file) throws IOException { + RandomAccessFile raf = null; + FileChannel fc = null; + try { + long fileSize = file.length(); + long blockSize = PackerCommon.BLOCK_SIZE_MAX; + long offset = Math.max(0, fileSize - blockSize); + raf = new RandomAccessFile(file, "r"); + fc = raf.getChannel(); + byte[] magic = PackerCommon.BLOCK_MAGIC.getBytes(PackerCommon.UTF8); + MappedByteBuffer buffer = fc.map(MapMode.READ_ONLY, offset, blockSize); + buffer.order(ByteOrder.LITTLE_ENDIAN); + KMPMatch kmp = new KMPMatch(magic); + int index = kmp.find(buffer); + if (index < 0) { + return null; + } +// System.out.println("index=" + index + " offset=" +// + (size - blockSize + index)); + byte[] actual = new byte[magic.length]; + buffer.position(index - 1); + buffer.get(actual); +// System.out.println("actual=" + new String(actual, "UTF-8")); + int len = buffer.getInt(); +// System.out.println("payload length=" + len); + if (len < 0 || len > blockSize) { + return null; + } + byte[] payload = new byte[len]; + buffer.get(payload); +// System.out.println("payload=" + payloadStr); + return new String(payload, PackerCommon.UTF8); + } finally { + if (fc != null) { + fc.close(); + } + if (raf != null) { + raf.close(); + } + } + } +} diff --git a/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java index 5923fbb..f52dc95 100644 --- a/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java +++ b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java @@ -36,11 +36,17 @@ public class PackerCommon { public static final int CHANNEL_BLOCK_ID = 0x7a786b21; // "zxk!" // channel info key public static final String CHANNEL_KEY = "CHANNEL"; + // zip block size max + public static final int BLOCK_SIZE_MAX = 0x100000; public static String readChannel(File file) throws IOException { return readValue(file, CHANNEL_KEY, CHANNEL_BLOCK_ID); } + public static String readChannel2(File file) throws IOException { + return KMPReader.readChannel(file); + } + public static void writeChannel(File file, String channel) throws IOException { writeValue(file, CHANNEL_KEY, channel, CHANNEL_BLOCK_ID); @@ -177,8 +183,7 @@ static ByteBuffer wrapPayload(byte[] payload) return buffer; } - // package visible for test - static String mapToString(Map map) + public static String mapToString(Map map) throws IOException { final StringBuilder builder = new StringBuilder(); for (Entry entry : map.entrySet()) { @@ -188,8 +193,7 @@ static String mapToString(Map map) return builder.toString(); } - // package visible for test - static Map mapFromString(final String string) { + public static Map mapFromString(final String string) { if (string == null || string.length() == 0) { return null; } diff --git a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java index 9fc5b9a..4ae64a4 100644 --- a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java +++ b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java @@ -52,12 +52,12 @@ void checkApkVerified(File f) { } public void testFileExists() { - File file = new File("data/test.apk"); + File file = new File("../tools/test.apk"); assertTrue(file.exists()); } public void testFileCopy() throws IOException { - File f1 = new File("data/test.apk"); + File f1 = new File("../tools/test.apk"); File f2 = newTestFile(); assertTrue(f2.exists()); assertTrue(f2.getName().endsWith(".apk")); @@ -227,5 +227,16 @@ public void testChannelWriteRead() throws IOException { checkApkVerified(f); } + public void testKMPReader() throws IOException { + File f = newTestFile(); + PackerCommon.writeChannel(f, "Hello2"); + assertEquals("Hello2", PackerCommon.readChannel2(f)); + PackerCommon.writeChannel(f, "中文@#$222"); + assertEquals("中文@#$222", PackerCommon.readChannel2(f)); + PackerCommon.writeChannel(f, "中文 C2222"); + assertEquals("中文 C2222", PackerCommon.readChannel2(f)); + checkApkVerified(f); + } + } diff --git a/common/src/test/java/com/mcxiaoke/packer/common/TestUtils.java b/common/src/test/java/com/mcxiaoke/packer/common/TestUtils.java index 605e36d..435b04d 100644 --- a/common/src/test/java/com/mcxiaoke/packer/common/TestUtils.java +++ b/common/src/test/java/com/mcxiaoke/packer/common/TestUtils.java @@ -54,11 +54,11 @@ public static String toHex(byte[] bytes) { } public static File newTestFile() throws IOException { - File dir = new File("data"); + File dir = new File("../tools/"); File file = new File(dir, "test.apk"); - File tempfile = new File(dir, System.currentTimeMillis() + "-test.apk"); - FileUtils.copyFile(file, tempfile); - return tempfile; + File tf = new File(dir, System.currentTimeMillis() + "-test.apk"); + FileUtils.copyFile(file, tf); + return tf; } private static int counter = 0; From 2ad507e4301bfc6017b7991bbcbf20103e4b5709 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 15:17:31 +0800 Subject: [PATCH 39/67] fix plugin output dir --- gradle.properties | 2 +- .../src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy | 2 +- sample/build.gradle | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 08ea2f7..7ed8620 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.8.8-SNAPSHOT +VERSION_NAME=1.8.9-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy index e015d44..8a0d3e8 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy @@ -65,7 +65,7 @@ class GradleTask extends DefaultTask { outputDir = extension.archiveOutput } if (outputDir == null) { - outputDir = new File(project.buildDir, Const.DEFAULT_OUTPUT) + outputDir = new File(project.rootProject.buildDir, Const.DEFAULT_OUTPUT) } if (!outputDir.exists()) { outputDir.mkdirs() diff --git a/sample/build.gradle b/sample/build.gradle index 46e976a..eb2b46d 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.8.8-SNAPSHOT' + ext.packer_version = '1.8.9-SNAPSHOT' repositories { maven { url '/tmp/repo/' } @@ -37,7 +37,7 @@ dependencies { //packer-begin packer { - archiveNameFormat = '${projectName}-${buildType}-v${versionName}-${channel}' + archiveNameFormat = '${appPkg}-${buildType}-v${versionName}-${channel}' archiveOutput = new File(project.rootProject.buildDir, "apks") // channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', // 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] From b555a744c3600f1f84f380bf1a9b94273b1c87a6 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 16:26:44 +0800 Subject: [PATCH 40/67] tweak channels parse and escape --- .../java/com/mcxiaoke/packer/cli/Helper.java | 49 +++++++++++++--- .../java/com/mcxiaoke/packer/cli/Main.java | 10 ++-- .../mcxiaoke/packer/common/PackerCommon.java | 2 +- gradle.properties | 2 +- .../mcxiaoke/packer/ng/GradleExtension.groovy | 2 +- .../com/mcxiaoke/packer/ng/GradleTask.groovy | 56 +++++-------------- sample/build.gradle | 2 +- .../mcxiaoke/packer/samples/MainActivity.java | 2 +- 8 files changed, 67 insertions(+), 58 deletions(-) diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Helper.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Helper.java index cf6e330..9dace2c 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/Helper.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Helper.java @@ -11,7 +11,11 @@ import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; /** * User: mcxiaoke @@ -19,24 +23,55 @@ * Time: 16:52 */ -class Helper { - public static List readChannels(final File file) throws IOException { - final List markets = new ArrayList(); +public class Helper { + + public static Set readChannels(String value) throws IOException { + if (value.startsWith("@")) { + return parseChannels(new File(value.substring(1))); + } else { + return parseChannels(value); + } + } + + public static Set parseChannels(final File file) throws IOException { + final List channels = new ArrayList<>(); FileReader fr = new FileReader(file); BufferedReader br = new BufferedReader(fr); String line; while ((line = br.readLine()) != null) { String parts[] = line.split("#"); if (parts.length > 0) { - final String market = parts[0].trim(); - if (market.length() > 0) { - markets.add(market); + final String ch = parts[0].trim(); + if (ch.length() > 0) { + channels.add(ch); } } } br.close(); fr.close(); - return markets; + return escape(channels); + } + + public static Set parseChannels(String text) { + String[] lines = text.split(","); + List channels = new ArrayList<>(); + for (String line : lines) { + String ch = line.trim(); + if (ch.length() > 0) { + channels.add(ch); + } + } + return escape(channels); + } + + public static Set escape(Collection cs) { + // filter invalid chars for filename + Pattern p = Pattern.compile("[\\\\/:*?\"'<>|]"); + Set set = new HashSet<>(); + for (String s : cs) { + set.add(p.matcher(s).replaceAll("_")); + } + return set; } public static void copyFile(File src, File dest) throws IOException { diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java index a2fbc14..9b13c66 100644 --- a/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java @@ -5,6 +5,7 @@ import java.io.File; import java.io.IOException; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Locale; @@ -60,7 +61,7 @@ private static void generate(String[] params) throws Exception { System.out.println("========== APK Packer =========="); // --channels=a,b,c, -c (list mode) // --channels=@list.txt -c (file mode) - List channels = null; + Collection channels = null; // --input, -i (input apk file) File apkFile = null; // --output, -o (output directory) @@ -77,10 +78,9 @@ private static void generate(String[] params) throws Exception { || "c".equals(name)) { String value = optionsParser.getRequiredValue("Channels file(@) or list(,)."); if (value.startsWith("@")) { - value = value.substring(1); - channels = Helper.readChannels(new File(value)); + channels = Helper.parseChannels(new File(value.substring(1))); } else { - channels = Arrays.asList(value.split(",")); + channels = Helper.parseChannels(value); } } else if ("input".equals(name) || "i".equals(name)) { @@ -109,7 +109,7 @@ private static void generate(String[] params) throws Exception { doGenerate(apkFile, channels, outputDir); } - private static void doGenerate(File apkFile, List channels, File outputDir) + private static void doGenerate(File apkFile, Collection channels, File outputDir) throws IOException { if (apkFile == null || !apkFile.exists() diff --git a/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java index f52dc95..b24f463 100644 --- a/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java +++ b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java @@ -43,7 +43,7 @@ public static String readChannel(File file) throws IOException { return readValue(file, CHANNEL_KEY, CHANNEL_BLOCK_ID); } - public static String readChannel2(File file) throws IOException { + static String readChannel2(File file) throws IOException { return KMPReader.readChannel(file); } diff --git a/gradle.properties b/gradle.properties index 7ed8620..e0bbdbd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.8.9-SNAPSHOT +VERSION_NAME=1.8.14-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy index 05648b8..1e90dc5 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy @@ -3,7 +3,7 @@ package com.mcxiaoke.packer.ng class GradleExtension { File archiveOutput String archiveNameFormat - List channelList; + Set channelList; File channelFile; Map channelMap; diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy index 8a0d3e8..1cbb545 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy @@ -2,7 +2,7 @@ package com.mcxiaoke.packer.ng import com.android.build.gradle.api.BaseVariant import com.mcxiaoke.packer.cli.Bridge -import groovy.io.FileType +import com.mcxiaoke.packer.cli.Helper import groovy.text.SimpleTemplateEngine import groovy.text.Template import org.gradle.api.DefaultTask @@ -10,7 +10,6 @@ import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction import java.text.SimpleDateFormat -import java.util.regex.Pattern /** * User: mcxiaoke @@ -83,12 +82,7 @@ class GradleTask extends DefaultTask { outputDir.mkdirs() } else { logger.info(":${name} delete old APKs in ${outputDir.absolutePath}") - // delete old APKs - outputDir.eachFile(FileType.FILES) { file -> - if (file.getName().endsWith(".apk")) { - file.delete() - } - } + Helper.deleteAPKs(outputDir) } return outputDir } @@ -98,7 +92,7 @@ class GradleTask extends DefaultTask { // -P channels=@channels.txt // channelList = [ch1,ch2,ch3] // channelFile = project.file("channels.txt") - List channels = [] + Collection channels = [] // check command line property def propValue = project.findProperty(Const.PROP_CHANNELS) if (propValue != null) { @@ -111,20 +105,20 @@ class GradleTask extends DefaultTask { if (!f.isFile() || !f.canRead()) { throw new PluginException("channel file not exists: '${f.absolutePath}'") } - channels = readChannels(f) + channels = Helper.parseChannels(f) } else { throw new PluginException("invalid channels property: '${prop}'") } } else { - channels = prop.split(",") + channels = Helper.parseChannels(prop); } if (channels == null || channels.isEmpty()) { throw new PluginException("invalid channels property: '${prop}'") } - return escape(channels) + return channels } if (extension.channelList != null) { - channels = extension.channelList + channels = Helper.escape(extension.channelList) logger.info(":${project.name} ext.channelList: ${channels}") } else if (extension.channelMap != null) { String flavorName = variant.flavorName @@ -134,7 +128,7 @@ class GradleTask extends DefaultTask { throw new PluginException("channel file not exists: '${f.absolutePath}'") } if (f != null && f.isFile()) { - channels = readChannels(f) + channels = Helper.parseChannels(f) } } else if (extension.channelFile != null) { File f = extension.channelFile @@ -142,12 +136,12 @@ class GradleTask extends DefaultTask { if (!f.isFile()) { throw new PluginException("channel file not exists: '${f.absolutePath}'") } - channels = readChannels(f) + channels = Helper.parseChannels(f) } if (channels == null || channels.isEmpty()) { throw new PluginException("No channels found") } - return escape(channels) + return channels } @@ -175,16 +169,16 @@ class GradleTask extends DefaultTask { println("Variant: ${variant.name}") println("Input: ${apkFile.path}") println("Output: ${outputDir.path}") - println("Channels: [${channels.join('/')}]") + println("Channels: [${channels.join(' ')}]") for (String channel : channels) { File tempFile = new File(outputDir, "tmp-${channel}.apk") - copyTo(apkFile, tempFile) try { + Helper.copyFile(apkFile, tempFile) Bridge.writeChannel(tempFile, channel) String apkName = buildApkName(channel, tempFile, template) File finalFile = new File(outputDir, apkName) if (Bridge.verifyChannel(tempFile, channel)) { - println("--> Generating: ${apkName}") + println("Generating: ${apkName}") tempFile.renameTo(finalFile) logger.info("Generated: ${finalFile}") } else { @@ -219,31 +213,11 @@ class GradleTask extends DefaultTask { return template.make(nameMap).toString() + '.apk' } + /* static Set escape(Collection cs) { // filter invalid chars for filename Pattern pattern = ~/[\/:*?"'<>|]/ return cs.collect { it.replaceAll(pattern, "_") }.toSet() } - - static List readChannels(File file) { - List channels = [] - file.eachLine { line, number -> - String[] parts = line.split('#') - if (parts && parts[0]) { - def c = parts[0].trim() - if (c) { - channels.add(c) - } - } - } - return channels - } - - static void copyTo(File src, File dest) { - def input = src.newInputStream() - def output = dest.newOutputStream() - output << input - input.close() - output.close() - } + */ } diff --git a/sample/build.gradle b/sample/build.gradle index eb2b46d..f204072 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.8.9-SNAPSHOT' + ext.packer_version = '1.8.14-SNAPSHOT' repositories { maven { url '/tmp/repo/' } diff --git a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java index c0610a1..81b67ee 100644 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java +++ b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java @@ -65,7 +65,7 @@ private void addAppInfoSection() { StringBuilder builder = new StringBuilder(); builder.append("[AppInfo]\n"); builder.append("SourceDir: ").append(getSourceDir(this)).append("\n"); - builder.append("Market: ").append(PackerNg.getChannel(this)).append("\n"); + builder.append("Channel: ").append(PackerNg.getChannel(this)).append("\n"); builder.append("Name: ").append(getString(info.labelRes)).append("\n"); builder.append("Package: ").append(BuildConfig.APPLICATION_ID).append("\n"); builder.append("VersionCode: ").append(BuildConfig.VERSION_CODE).append("\n"); From c6e668ff68bfbe5e49d340d9da04798eaac45f97 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 16:28:32 +0800 Subject: [PATCH 41/67] tweak python script --- tools/packer-ng-v2.py | 71 ++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index a218b64..22c87f7 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -2,7 +2,7 @@ # @Author: mcxiaoke # @Date: 2017-06-06 14:03:18 # @Last Modified by: mcxiaoke -# @Last Modified time: 2017-06-09 11:41:59 +# @Last Modified time: 2017-06-09 16:27:15 from __future__ import print_function import os import sys @@ -40,6 +40,8 @@ UINT16_MAX_VALUE = 0xffff # 65535 +BlOCK_MAX_SIZE = 0x100000 # 1m=1024k + APK_SIG_BLOCK_MAGIC = 'APK Sig Block 42' APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42 APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041 @@ -136,24 +138,7 @@ def __init__(self, cdStartOffset, ##################################################################### -def getChannel(apk): - apk = os.path.abspath(apk) - logger.debug('apk:%s', os.path.basename(apk)) - try: - zp = zipfile.ZipFile(apk) - zp.testzip() - values = findValues(apk) - if values: - channel = values.get(PLUGIN_CHANNEL_KEY) - logger.debug('channel:%s', channel) - return channel - else: - logger.debug('channel not found') - except Exception as e: - logger.error('%s: %s', type(e).__name__, e.message) - - -def findValues(apk): +def parseValues(content): ''' PLUGIN BLOCK LAYOUT OFFSET DATA TYPE DESCRIPTION @@ -162,7 +147,6 @@ def findValues(apk): @+20 payload payload data bytes @-4 payload length same as @+16 4 bytes ''' - content = findBlock1(apk) magicLen = len(PLUGIN_BLOCK_MAGIC) logger.debug('content:%s', content) if not content or len(content) < magicLen + 8: @@ -224,7 +208,7 @@ def findBySigningMagic(apk): d = ByteDecoder(mm) size = mm.size() logger.debug('file size=%s', size) - offset = size - UINT16_MAX_VALUE + offset = size - BlOCK_MAX_SIZE logger.debug('file offset=%s', offset) index = mm.find(APK_SIG_BLOCK_MAGIC, offset) if index == -1: @@ -487,16 +471,41 @@ def timed(*args, **kw): return timed -@timeit -def test(apk): - for i in range(500): - channel = getChannel(apk) - - def to_hex(s): return " ".join("{:02x}".format(ord(c)) for c in s) if s else "" +def getChannel(apk): + apk = os.path.abspath(apk) + logger.debug('apk:%s', os.path.basename(apk)) + try: + zp = zipfile.ZipFile(apk) + zp.testzip() + content = findBlock3(apk) + values = parseValues(content) + if values: + channel = values.get(PLUGIN_CHANNEL_KEY) + logger.debug('channel:%s', channel) + return channel + else: + logger.debug('channel not found') + except Exception as e: + logger.error('%s: %s', type(e).__name__, e.message) + + +def showInfo(apk): + print('File: \t\t{}'.format(os.path.basename(apk))) + print('Size: \t\t{}'.format(os.path.getsize(apk))) + try: + from apkinfo import APK + info = APK(apk) + print('Package: \t{}'.format(info.get_package())) + print('Version: \t{}'.format(info.get_version_name())) + print('Build: \t\t{}'.format(info.get_version_code())) + except Exception as e: + pass + + def main(): logger.debug('AUTHOR:%s', AUTHOR) logger.debug('VERSION:%s', VERSION) @@ -505,15 +514,9 @@ def main(): print('Usage: {} app.apk'.format(prog)) sys.exit(1) apk = os.path.abspath(sys.argv[1]) - from apkinfo import APK - info = APK(apk) - print('File: \t\t{}'.format(os.path.basename(apk))) - print('Package: \t{}'.format(info.get_package())) - print('Version: \t{}'.format(info.get_version_name())) - print('Build: \t\t{}'.format(info.get_version_code())) channel = getChannel(apk) print('Channel: \t{}'.format(channel)) - # test(apk) + showInfo(apk) if __name__ == '__main__': main() From cace43f40fd680a14d120b144db1f59e6f50db7e Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 17:20:11 +0800 Subject: [PATCH 42/67] optimize python script --- tools/packer-ng-v2.py | 112 +++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index 22c87f7..1722eaf 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -2,7 +2,7 @@ # @Author: mcxiaoke # @Date: 2017-06-06 14:03:18 # @Last Modified by: mcxiaoke -# @Last Modified time: 2017-06-09 16:27:15 +# @Last Modified time: 2017-06-09 17:19:52 from __future__ import print_function import os import sys @@ -149,42 +149,49 @@ def parseValues(content): ''' magicLen = len(PLUGIN_BLOCK_MAGIC) logger.debug('content:%s', content) - if not content or len(content) < magicLen + 8: + if not content or len(content) < magicLen + 4 * 2: return None - content = content[magicLen+4:-4] + content = content[magicLen + 4: -4] values = dict(line.split(SEP_KV) for line in content.split(SEP_LINE) if line.strip()) logger.debug('values:%s', values) return values -def findBlock1(apk): - # # search Plugin Magic words +def createMap(apk): with open(apk, "rb") as f: - mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) - d = ByteDecoder(mm) - size = mm.size() + size = os.path.getsize(apk) + offset = max(0, size - BlOCK_MAX_SIZE) + offset = offset - offset % mmap.PAGESIZE logger.debug('file size=%s', size) - magicLen = len(PLUGIN_BLOCK_MAGIC) - start = mm.rfind(PLUGIN_BLOCK_MAGIC) - # if start == -1: - # raise MagicNotFoundException( - # 'Packer Ng Magic words not found') - if start == -1: - return None - logger.debug('magic start offset=%s', start) - magic = ''.join(d.getChars(start, magicLen)) - logger.debug('magic start string=%s', magic) - payloadLen = d.getInt(start + magicLen) - logger.debug('magic payloadLen1=%s', payloadLen) - - end = start + magicLen + 4 + payloadLen + 4 - logger.debug('magic end offset=%s', end) - logger.debug('magic payloadLen2=%s', d.getInt(end-4)) - - block = mm[start:end] - mm.close() - return block + logger.debug('file offset=%s', offset) + return mmap.mmap(f.fileno(), + length=BlOCK_MAX_SIZE, + offset=offset, + access=mmap.ACCESS_READ) + + +def findBlock1(apk): + # # search Plugin Magic words + mm = createMap(apk) + magicLen = len(PLUGIN_BLOCK_MAGIC) + start = mm.rfind(PLUGIN_BLOCK_MAGIC) + if start == -1: + return None + d = ByteDecoder(mm) + logger.debug('magic start offset=%s', start) + magic = ''.join(d.getChars(start, magicLen)) + logger.debug('magic start string=%s', magic) + payloadLen = d.getInt(start + magicLen) + logger.debug('magic payloadLen1=%s', payloadLen) + + end = start + magicLen + 4 + payloadLen + 4 + logger.debug('magic end offset=%s', end) + logger.debug('magic payloadLen2=%s', d.getInt(end-4)) + + block = mm[start:end] + mm.close() + return block def findBlock2(apk): @@ -203,28 +210,23 @@ def findBlock3(apk): def findBySigningMagic(apk): # findApkSigningBlockUsingSigningMagic - with open(apk, "rb") as f: - mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) - d = ByteDecoder(mm) - size = mm.size() - logger.debug('file size=%s', size) - offset = size - BlOCK_MAX_SIZE - logger.debug('file offset=%s', offset) - index = mm.find(APK_SIG_BLOCK_MAGIC, offset) - if index == -1: - raise MagicNotFoundException( - 'APK Signing Block Magic not found') - logger.debug('magic index=%s', index) - logger.debug('magic string=%s', ''.join(d.getChars(index, 16))) - bEnd = index + 16 - logger.debug('block end=%s', bEnd) - bSize = d.getLong(bEnd-24)+8 - logger.debug('block size=%s', bSize) - bStart = bEnd - bSize - logger.debug('block start=%s', bStart) - block = mm[bStart:bEnd] - mm.close() - return block + mm = createMap(apk) + index = mm.rfind(APK_SIG_BLOCK_MAGIC) + if index == -1: + raise MagicNotFoundException( + 'APK Signing Block Magic not found') + d = ByteDecoder(mm) + logger.debug('magic index=%s', index) + logger.debug('magic string=%s', ''.join(d.getChars(index, 16))) + bEnd = index + 16 + logger.debug('block end=%s', bEnd) + bSize = d.getLong(bEnd-24)+8 + logger.debug('block size=%s', bSize) + bStart = bEnd - bSize + logger.debug('block start=%s', bStart) + block = mm[bStart:bEnd] + mm.close() + return block def findByZipSections(apk): @@ -260,7 +262,7 @@ def findByZipSections(apk): # logger.debug('footer:%s',to_hex(footer)) fd = ByteDecoder(footer) magic = ''.join(fd.getChars(8, 16)) - logger.debug('magic str:%s', magic) + # logger.debug('magic str:%s', magic) lo = fd.getLong(8) hi = fd.getLong(16) logger.debug('magic lo:%s', hex(lo)) @@ -477,7 +479,7 @@ def to_hex(s): def getChannel(apk): apk = os.path.abspath(apk) - logger.debug('apk:%s', os.path.basename(apk)) + logger.debug('apk:%s', apk) try: zp = zipfile.ZipFile(apk) zp.testzip() @@ -490,18 +492,18 @@ def getChannel(apk): else: logger.debug('channel not found') except Exception as e: - logger.error('%s: %s', type(e).__name__, e.message) + logger.error('%s: %s', type(e).__name__, e) def showInfo(apk): - print('File: \t\t{}'.format(os.path.basename(apk))) - print('Size: \t\t{}'.format(os.path.getsize(apk))) try: from apkinfo import APK info = APK(apk) print('Package: \t{}'.format(info.get_package())) print('Version: \t{}'.format(info.get_version_name())) print('Build: \t\t{}'.format(info.get_version_code())) + print('File: \t\t{}'.format(os.path.basename(apk))) + print('Size: \t\t{}'.format(os.path.getsize(apk))) except Exception as e: pass From f71b8d4e090791181872d55fcec4f93d41160d33 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 17:35:59 +0800 Subject: [PATCH 43/67] reduce most sample size --- sample/build.gradle | 10 +- sample/src/main/AndroidManifest.xml | 6 +- .../mcxiaoke/packer/samples/MainActivity.java | 248 +----------------- .../com/mcxiaoke/packer/samples/ResUtils.java | 61 ----- .../mcxiaoke/packer/samples/ViewUtils.java | 185 ------------- .../res/drawable-hdpi/drawer_shadow.9.png | Bin 161 -> 0 bytes .../src/main/res/drawable-hdpi/ic_drawer.png | Bin 2829 -> 0 bytes .../main/res/drawable-hdpi/ic_launcher.png | Bin 9397 -> 0 bytes .../res/drawable-mdpi/drawer_shadow.9.png | Bin 142 -> 0 bytes .../src/main/res/drawable-mdpi/ic_drawer.png | Bin 2820 -> 0 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 5237 -> 0 bytes .../res/drawable-xhdpi/drawer_shadow.9.png | Bin 174 -> 0 bytes .../src/main/res/drawable-xhdpi/ic_drawer.png | Bin 2836 -> 0 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 14383 -> 0 bytes .../res/drawable-xxhdpi/drawer_shadow.9.png | Bin 208 -> 0 bytes .../main/res/drawable-xxhdpi/ic_drawer.png | Bin 202 -> 0 bytes sample/src/main/res/layout/act_main.xml | 16 -- sample/src/main/res/values-w820dp/dimens.xml | 6 - sample/src/main/res/values/dimens.xml | 9 - sample/src/main/res/values/strings.xml | 6 - sample/src/main/res/values/styles.xml | 8 - 21 files changed, 14 insertions(+), 541 deletions(-) delete mode 100644 sample/src/main/java/com/mcxiaoke/packer/samples/ResUtils.java delete mode 100644 sample/src/main/java/com/mcxiaoke/packer/samples/ViewUtils.java delete mode 100644 sample/src/main/res/drawable-hdpi/drawer_shadow.9.png delete mode 100644 sample/src/main/res/drawable-hdpi/ic_drawer.png delete mode 100644 sample/src/main/res/drawable-hdpi/ic_launcher.png delete mode 100644 sample/src/main/res/drawable-mdpi/drawer_shadow.9.png delete mode 100644 sample/src/main/res/drawable-mdpi/ic_drawer.png delete mode 100644 sample/src/main/res/drawable-mdpi/ic_launcher.png delete mode 100644 sample/src/main/res/drawable-xhdpi/drawer_shadow.9.png delete mode 100644 sample/src/main/res/drawable-xhdpi/ic_drawer.png delete mode 100644 sample/src/main/res/drawable-xhdpi/ic_launcher.png delete mode 100644 sample/src/main/res/drawable-xxhdpi/drawer_shadow.9.png delete mode 100644 sample/src/main/res/drawable-xxhdpi/ic_drawer.png delete mode 100644 sample/src/main/res/layout/act_main.xml delete mode 100644 sample/src/main/res/values-w820dp/dimens.xml delete mode 100644 sample/src/main/res/values/dimens.xml delete mode 100644 sample/src/main/res/values/strings.xml delete mode 100644 sample/src/main/res/values/styles.xml diff --git a/sample/build.gradle b/sample/build.gradle index f204072..fe0896c 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.8.14-SNAPSHOT' + ext.packer_version = '1.8.15-SNAPSHOT' repositories { maven { url '/tmp/repo/' } @@ -24,15 +24,7 @@ apply plugin: 'packer' // https://code.google.com/p/android/issues/detail?id=171089 dependencies { - // compile project(':helper') compile "com.mcxiaoke.packer-ng:helper:$packer_version" - compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:support-v4:25.3.1' - compile 'com.android.support:appcompat-v7:25.3.1' - compile 'com.jakewharton:butterknife:6.0.0' - compile('com.mcxiaoke.next:core:1.5.0') { - exclude group: 'com.android.support', module: 'support-v4' - } } //packer-begin diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 626aa41..00687dd 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -5,18 +5,16 @@ - + android:label="PackerNg"> + android:label="PackerNg"> diff --git a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java index 81b67ee..11aec72 100644 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java +++ b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java @@ -1,253 +1,27 @@ package com.mcxiaoke.packer.samples; -import android.annotation.SuppressLint; -import android.app.ActivityManager; -import android.app.ActivityManager.MemoryInfo; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.graphics.Point; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.Build; -import android.os.Build.VERSION_CODES; +import android.app.Activity; import android.os.Bundle; -import android.support.v7.app.ActionBarActivity; -import android.support.v7.app.AppCompatActivity; -import android.util.DisplayMetrics; -import android.view.Display; -import android.view.ViewGroup; +import android.util.TypedValue; +import android.view.Gravity; import android.view.ViewGroup.LayoutParams; import android.widget.TextView; -import butterknife.ButterKnife; -import butterknife.InjectView; -import com.mcxiaoke.next.utils.AndroidUtils; -import com.mcxiaoke.next.utils.LogUtils; -import com.mcxiaoke.next.utils.StringUtils; import com.mcxiaoke.packer.helper.PackerNg; -import com.mcxiaoke.packer.samples.BuildConfig; -import com.mcxiaoke.packer.samples.R; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Set; - -public class MainActivity extends AppCompatActivity { +public class MainActivity extends Activity { private static final String TAG = MainActivity.class.getSimpleName(); - @InjectView(R.id.container) - ViewGroup mContainer; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.act_main); - ButterKnife.inject(this); - addBuildConfigSection(); - addMetaDataSection(); - addAppInfoSection(); - addNetworkInfoSection(); - addDeviceInfoSection(); - addBuildPropsSection(); - - } - - private String getSourceDir(final Object context) { - return getApplicationInfo().sourceDir; - } - - private void addAppInfoSection() { - try { - final ApplicationInfo info = getApplicationInfo(); - StringBuilder builder = new StringBuilder(); - builder.append("[AppInfo]\n"); - builder.append("SourceDir: ").append(getSourceDir(this)).append("\n"); - builder.append("Channel: ").append(PackerNg.getChannel(this)).append("\n"); - builder.append("Name: ").append(getString(info.labelRes)).append("\n"); - builder.append("Package: ").append(BuildConfig.APPLICATION_ID).append("\n"); - builder.append("VersionCode: ").append(BuildConfig.VERSION_CODE).append("\n"); - builder.append("VersionName: ").append(BuildConfig.VERSION_NAME).append("\n"); - builder.append("ProcessName: ").append(info.processName).append("\n"); - builder.append("CodePath: ").append(getPackageCodePath()).append("\n"); - builder.append("SourceDir: ").append(info.sourceDir).append("\n"); - builder.append("PubSourceDir: ").append(info.publicSourceDir).append("\n"); - builder.append("DataDir: ").append(info.dataDir).append("\n"); - builder.append("Signature:\n"); - builder.append(AndroidUtils.getSignature(this)).append("\n"); - builder.append("\n"); - addSection(builder.toString()); - } catch (Exception e) { - e.printStackTrace(); - } - - - } - - private void addMetaDataSection() { - final PackageManager pm = getPackageManager(); - final String packageName = getPackageName(); - try { - final ApplicationInfo info = pm.getApplicationInfo(packageName, - PackageManager.GET_SIGNATURES | PackageManager.GET_META_DATA); - final Bundle bundle = info.metaData; - final StringBuilder builder = new StringBuilder(); - builder.append("[MetaData]\n"); - if (bundle != null) { - final Set keySet = bundle.keySet(); - for (final String key : keySet) { - builder.append(key).append("=").append(bundle.get(key)).append("\n"); - } - } - addSection(builder.toString()); - } catch (NameNotFoundException e) { - e.printStackTrace(); - } - } - - private void addNetworkInfoSection() { - StringBuilder builder = new StringBuilder(); - builder.append("[Network]\n"); - ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo info = cm.getActiveNetworkInfo(); - if (info != null) { - builder.append(info); - } - builder.append("\n\n"); - addSection(builder.toString()); - } - - @SuppressLint("NewApi") - private void addDeviceInfoSection() { - StringBuilder builder = new StringBuilder(); - builder.append("[Device]\n"); - - ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); - final MemoryInfo memoryInfo = new MemoryInfo(); - am.getMemoryInfo(memoryInfo); - if (AndroidUtils.hasJellyBean()) { - builder.append("Mem Total: ").append(StringUtils.getHumanReadableByteCount(memoryInfo.totalMem)).append("\n"); - } - builder.append("Mem Free: ").append(StringUtils.getHumanReadableByteCount(memoryInfo.availMem)).append("\n"); - builder.append("Mem Heap: ").append(am.getMemoryClass()).append("M\n"); - builder.append("Mem Low: ").append(memoryInfo.lowMemory).append("\n"); - Display display = getWindowManager().getDefaultDisplay(); - DisplayMetrics dm = new DisplayMetrics(); - //DisplayMetrics dm = getResources().getDisplayMetrics(); - display.getMetrics(dm); - - int statusBarHeightDp = ViewUtils.getStatusBarHeightInDp(this); - int systemBarHeightDp = ViewUtils.getSystemBarHeightInDp(this); - int statusBarHeight = ViewUtils.getStatusBarHeight(this); - int systemBarHeight = ViewUtils.getSystemBarHeight(this); - Point point = getScreenRawSize(display); - builder.append("statusBarHeightDp: ").append(statusBarHeightDp).append("\n"); - builder.append("systemBarHeightDp: ").append(systemBarHeightDp).append("\n"); - builder.append("statusBarHeightPx: ").append(statusBarHeight).append("\n"); - builder.append("systemBarHeightPx: ").append(systemBarHeight).append("\n"); - builder.append("screenWidth: ").append(point.x).append("\n"); - builder.append("screenHeight: ").append(point.y).append("\n"); - builder.append("WindowWidth: ").append(dm.widthPixels).append("\n"); - builder.append("WindowHeight: ").append(dm.heightPixels).append("\n"); - builder.append(toString2(dm)); - builder.append("\n"); - addSection(builder.toString()); - } - - private void addBuildConfigSection() { - StringBuilder builder = new StringBuilder(); - builder.append("[BuildConfig]\n"); - builder.append(toString(BuildConfig.class)); - builder.append("\n"); - addSection(builder.toString()); - } - - private void addBuildPropsSection() { - StringBuilder builder = new StringBuilder(); - builder.append("[System]\n"); - builder.append(toString(Build.VERSION.class)); - builder.append(toString(Build.class)); - builder.append("\n"); - addSection(builder.toString()); - } - - private LayoutParams TEXT_VIEW_LP = new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.WRAP_CONTENT); - - private void addSection(CharSequence text) { - TextView tv = new TextView(this); - tv.setLayoutParams(TEXT_VIEW_LP); - tv.setText(text); - tv.setTextIsSelectable(true); - mContainer.addView(tv); - } - - - @SuppressLint("NewApi") - public static Point getScreenRawSize(Display display) { - if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { - Point outPoint = new Point(); - DisplayMetrics metrics = new DisplayMetrics(); - display.getRealMetrics(metrics); - outPoint.x = metrics.widthPixels; - outPoint.y = metrics.heightPixels; - return outPoint; - } else { - Point outPoint = new Point(); - Method mGetRawH; - try { - mGetRawH = Display.class.getMethod("getRawHeight"); - Method mGetRawW = Display.class.getMethod("getRawWidth"); - outPoint.x = (Integer) mGetRawW.invoke(display); - outPoint.y = (Integer) mGetRawH.invoke(display); - return outPoint; - } catch (Throwable e) { - return new Point(0, 0); - } - } - } - - public static String toString(Class clazz) { - StringBuilder builder = new StringBuilder(); - final String newLine = System.getProperty("line.separator"); - Field[] fields = clazz.getDeclaredFields(); - for (Field field : fields) { - field.setAccessible(true); - String fieldName = field.getName(); - if (Modifier.isStatic(field.getModifiers())) { - LogUtils.v(TAG, "filed:" + fieldName); - try { - Object fieldValue = field.get(null); - builder.append(fieldName).append(": ").append(fieldValue).append(newLine); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - } - return builder.toString(); - } - - public static String toString2(Object object) { - Class clazz = object.getClass(); - StringBuilder builder = new StringBuilder(); - final String newLine = System.getProperty("line.separator"); - Field[] fields = clazz.getDeclaredFields(); - for (Field field : fields) { - field.setAccessible(true); - String fieldName = field.getName(); - if (!Modifier.isStatic(field.getModifiers())) { - LogUtils.v(TAG, "filed:" + fieldName); - try { - Object fieldValue = field.get(object); - builder.append(fieldName).append(": ").append(fieldValue).append(newLine); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - } - return builder.toString(); + TextView v = new TextView(this); + LayoutParams p = new LayoutParams(-1, -1); + setContentView(v, p); + v.setTextSize(TypedValue.COMPLEX_UNIT_SP, 40); + v.setGravity(Gravity.CENTER); + v.setPadding(40, 40, 40, 40); + v.setText(PackerNg.getChannel(this)); } } diff --git a/sample/src/main/java/com/mcxiaoke/packer/samples/ResUtils.java b/sample/src/main/java/com/mcxiaoke/packer/samples/ResUtils.java deleted file mode 100644 index 9d37104..0000000 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/ResUtils.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.mcxiaoke.packer.samples; - -import android.content.Context; - -/** - * User: mcxiaoke - * Date: 16/1/13 - * Time: 15:11 - */ -public class ResUtils { - - public static int getDrawableResId(Context context, String resName) { - return getResId(context, "drawable", resName); - } - - public static int getMenuResId(Context context, String resName) { - return getResId(context, "layout", resName); - } - - public static int getLayoutResId(Context context, String resName) { - return getResId(context, "layout", resName); - } - - public static int getAnimResId(Context context, String resName) { - return getResId(context, "anim", resName); - } - - public static int getAttrResId(Context context, String resName) { - return getResId(context, "attr", resName); - } - - public static int getStyleResId(Context context, String resName) { - return getResId(context, "style", resName); - } - - public static int getDimenResId(Context context, String resName) { - return getResId(context, "dimen", resName); - } - - public static int getColorResId(Context context, String resName) { - return getResId(context, "color", resName); - } - - public static int getRawResId(Context context, String resName) { - return getResId(context, "raw", resName); - } - - public static int getStringResId(Context context, String resName) { - return getResId(context, "string", resName); - } - - public static int getResourceId(Context context, String resName) { - return getResId(context, "id", resName); - } - - public static int getResId(Context context, String type, String resName) { - final String packageName = context.getPackageName(); - return context.getResources().getIdentifier(resName, type, packageName); - } - -} diff --git a/sample/src/main/java/com/mcxiaoke/packer/samples/ViewUtils.java b/sample/src/main/java/com/mcxiaoke/packer/samples/ViewUtils.java deleted file mode 100644 index 6777a90..0000000 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/ViewUtils.java +++ /dev/null @@ -1,185 +0,0 @@ -package com.mcxiaoke.packer.samples; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.Context; -import android.graphics.Point; -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.util.DisplayMetrics; -import android.util.TypedValue; -import android.view.Display; -import android.view.View; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; - -import java.lang.reflect.Method; - -/** - * User: mcxiaoke - * Date: 14-3-26 - * Time: 16:08 - */ -public class ViewUtils { - - public static ProgressBar createProgress(Context context) { - ProgressBar p = new ProgressBar(context); - p.setIndeterminate(true); - RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(40, 40); - lp.addRule(RelativeLayout.CENTER_IN_PARENT); - p.setLayoutParams(lp); - return p; - } - - // This intro hides the system bars. - @TargetApi(VERSION_CODES.KITKAT) - public static void hideSystemUI(Activity activity) { - // Set the IMMERSIVE flag. - // Set the content to appear under the system bars so that the content - // doesn't resize when the system bars hideSelf and show. - View decorView = activity.getWindow().getDecorView(); - decorView.setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hideSelf nav bar - | View.SYSTEM_UI_FLAG_FULLSCREEN // hideSelf status bar - | View.SYSTEM_UI_FLAG_IMMERSIVE - ); - } - - // This intro shows the system bars. It does this by removing all the flags -// except for the ones that make the content appear under the system bars. - @TargetApi(VERSION_CODES.KITKAT) - public static void showSystemUI(Activity activity) { - View decorView = activity.getWindow().getDecorView(); - decorView.setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - ); - } - - /** - * 23 * Returns true if view's layout direction is right-to-left. - * 24 * - * 25 * @param view the View whose layout is being considered - * 26 - */ - @SuppressLint("NewApi") - public static boolean isLayoutRtl(View view) { - if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { - return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - } else { - // All layouts are LTR before JB MR1. - return false; - } - } - - @SuppressLint("NewApi") - public static Point getScreenRawSize(Display display) { - if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) { - Point outPoint = new Point(); - DisplayMetrics metrics = new DisplayMetrics(); - display.getRealMetrics(metrics); - outPoint.x = metrics.widthPixels; - outPoint.y = metrics.heightPixels; - return outPoint; - } else { - Point outPoint = new Point(); - Method mGetRawH; - try { - mGetRawH = Display.class.getMethod("getRawHeight"); - Method mGetRawW = Display.class.getMethod("getRawWidth"); - outPoint.x = (Integer) mGetRawW.invoke(display); - outPoint.y = (Integer) mGetRawH.invoke(display); - return outPoint; - } catch (Throwable e) { - return new Point(0, 0); - } - } - } - - public static int getActionBarHeightInDp(Context context) { - int actionBarHeight = 0; - TypedValue tv = new TypedValue(); - final DisplayMetrics dm = context.getResources().getDisplayMetrics(); - if (Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { - if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, - true)) - actionBarHeight = (int) TypedValue.complexToFloat(tv.data); - } else { - tv.data = 48; - actionBarHeight = (int) TypedValue.complexToFloat(tv.data); - } - return actionBarHeight; - } - - public static int getActionBarHeight(Context context) { - int actionBarHeight = 0; - TypedValue tv = new TypedValue(); - final DisplayMetrics dm = context.getResources().getDisplayMetrics(); - if (Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) { - if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, - true)) - actionBarHeight = TypedValue.complexToDimensionPixelSize( - tv.data, dm); - } else { - tv.data = 48; - actionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, - dm); - } - return actionBarHeight; - } - - public static int getStatusBarHeight(Context context) { - int result = 0; - int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); - if (resourceId > 0) { - - result = context.getResources().getDimensionPixelSize(resourceId); - } - return result; - } - - public static int getSystemBarHeight(Context context) { - int result = 0; - int resourceId = context.getResources().getIdentifier("system_bar_height", "dimen", "android"); - - if (resourceId > 0) { - result = context.getResources().getDimensionPixelSize(resourceId); - } - return result; - } - - public static int getStatusBarHeightInDp(Context context) { - int result = 0; - int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); - if (resourceId > 0) { - result = getResourceValue(context, resourceId); - } - return result; - } - - public static int getSystemBarHeightInDp(Context context) { - int result = 0; - int resourceId = context.getResources().getIdentifier("system_bar_height", "dimen", "android"); - if (resourceId > 0) { - result = getResourceValue(context, resourceId); - } - return result; - } - - // temp variable - private static TypedValue mTmpValue = new TypedValue(); - - /** - * 获取资源中的数值,没有经过转换,比如dp,sp等 - */ - public static int getResourceValue(Context context, int resId) { - TypedValue value = mTmpValue; - context.getResources().getValue(resId, value, true); - return (int) TypedValue.complexToFloat(value.data); - } -} diff --git a/sample/src/main/res/drawable-hdpi/drawer_shadow.9.png b/sample/src/main/res/drawable-hdpi/drawer_shadow.9.png deleted file mode 100644 index 236bff558af07faa3921ba35e2515edf62d04bb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 161 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 diff --git a/sample/src/main/res/drawable-xhdpi/drawer_shadow.9.png b/sample/src/main/res/drawable-xhdpi/drawer_shadow.9.png deleted file mode 100644 index fabe9d96563785c7d6b008bb3d8da25e816c343c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^{6Or)!3HEd1bTh|DVAa<&kznEsNqQI0P;BtJR*x3 z7`Qt@n9=;?>9s(?08bak5Rc<;uP@|nU=UzFz%6x@#ly{E6Z5RU$2Hhw*i>Gs`(~~C zr~2_>(e4Ka`gpa)&de}Ks*u{@U5E@m-v9X3<$3NT3r3p*`<7|@n1Ecw;OXk;vd$@? F2>^8yH@N@+ diff --git a/sample/src/main/res/drawable-xhdpi/ic_drawer.png b/sample/src/main/res/drawable-xhdpi/ic_drawer.png deleted file mode 100644 index a5fa74def4b40d7eb6826da05bd5e12b836cb999..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2836 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 diff --git a/sample/src/main/res/drawable-xxhdpi/drawer_shadow.9.png b/sample/src/main/res/drawable-xxhdpi/drawer_shadow.9.png deleted file mode 100644 index b91e9d7f285e8110ba3ba4e72cc6f0416eb3a30a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^VnCe4!3HEl*p=S^DajJoh?3y^w370~qErUQl>DSr z1<%~X^wgl##FWaylc_d9MMa)2jv*QM-d<4Ta$*#5x!8O#VbcvCx78OjjCM19-`|n` zlcQ;yJWqK-!X?h+v^9}Es?F+hJ07=b>sdT*QRcgm+^%b8|A7C`|A*gYPjm3$2miRv cpZdzQt0C%<%j~>EK-(ESUHx3vIVCg!0CP)0sQ>@~ diff --git a/sample/src/main/res/drawable-xxhdpi/ic_drawer.png b/sample/src/main/res/drawable-xxhdpi/ic_drawer.png deleted file mode 100644 index 9c4685d6e046ce6c450c19426dce627a88718bfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWND5#L^ z5#-Cjkf+YTP`sXjk!=$LL!~YQgWnqlM%L3n1JW26goK+9UQq_B4e)ev49U3n_PU`U zg8>io#t-TVJM=pEGT)onF!&ZOd;8}O149hcq!N%?hK6(oZAK7-f#HS|n9?%@Qw{w* cAUc7wfPvd9>y+w~O{pNhr>mdKI;Vst074Kf@&Et; diff --git a/sample/src/main/res/layout/act_main.xml b/sample/src/main/res/layout/act_main.xml deleted file mode 100644 index b8e9a4b..0000000 --- a/sample/src/main/res/layout/act_main.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/sample/src/main/res/values-w820dp/dimens.xml b/sample/src/main/res/values-w820dp/dimens.xml deleted file mode 100644 index 63fc816..0000000 --- a/sample/src/main/res/values-w820dp/dimens.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - 64dp - diff --git a/sample/src/main/res/values/dimens.xml b/sample/src/main/res/values/dimens.xml deleted file mode 100644 index 074e7a0..0000000 --- a/sample/src/main/res/values/dimens.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - 16dp - 16dp - - - 240dp - diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml deleted file mode 100644 index 4f7813f..0000000 --- a/sample/src/main/res/values/strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - PackerNg - - diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml deleted file mode 100644 index 92795d1..0000000 --- a/sample/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - From a226dc74fb508e86664c723ff41cc68339c12055 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 17:36:14 +0800 Subject: [PATCH 44/67] public read channel method --- gradle.properties | 2 +- .../main/java/com/mcxiaoke/packer/helper/PackerNg.java | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e0bbdbd..024bbdc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.8.14-SNAPSHOT +VERSION_NAME=1.8.15-SNAPSHOT VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java index adba9b1..3096dbc 100644 --- a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java +++ b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java @@ -5,6 +5,7 @@ import com.mcxiaoke.packer.common.PackerCommon; import java.io.File; +import java.io.IOException; /** * User: mcxiaoke @@ -16,6 +17,14 @@ public final class PackerNg { private static final String EMPTY_STRING = ""; private static String sCachedChannel; + public static String readChannel(final File file) { + try { + return PackerCommon.readChannel(file); + } catch (IOException e) { + return EMPTY_STRING; + } + } + public static String getChannel(final Context context) { return getChannel(context, EMPTY_STRING); } From 39f60beb381fca188dd995aa7206619e5e29efd1 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 17:45:43 +0800 Subject: [PATCH 45/67] sample show other package channel --- .../mcxiaoke/packer/samples/MainActivity.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java index 11aec72..78df37a 100644 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java +++ b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java @@ -1,16 +1,22 @@ package com.mcxiaoke.packer.samples; import android.app.Activity; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.Bundle; +import android.util.Log; import android.util.TypedValue; import android.view.Gravity; import android.view.ViewGroup.LayoutParams; import android.widget.TextView; import com.mcxiaoke.packer.helper.PackerNg; +import java.io.File; +import java.util.List; + public class MainActivity extends Activity { - private static final String TAG = MainActivity.class.getSimpleName(); + private static final String TAG = "PackerNg"; @Override protected void onCreate(Bundle savedInstanceState) { @@ -22,6 +28,16 @@ protected void onCreate(Bundle savedInstanceState) { v.setGravity(Gravity.CENTER); v.setPadding(40, 40, 40, 40); v.setText(PackerNg.getChannel(this)); + + PackageManager pm = getPackageManager(); + List apps = pm.getInstalledApplications(PackageManager.GET_META_DATA); + for (ApplicationInfo app : apps) { + if (app.packageName.startsWith("com.douban.")) { + Log.d("TAG", "app=" + app.packageName + ", channel=" + + PackerNg.readChannel(new File(app.sourceDir))); + } + } + } } From cb27a2d9b0fc6aeee0fb960338072b7a13ffe446 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 17:49:46 +0800 Subject: [PATCH 46/67] release v1.9.0 for test --- gradle.properties | 2 +- readme.md | 4 ++-- sample/build.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index 024bbdc..373f3d0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.8.15-SNAPSHOT +VERSION_NAME=1.9.0 VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/readme.md b/readme.md index bc17d22..214e612 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -PackerNg V2 +PackerNg V2 (即将发布) ======== 极速渠道打包工具 @@ -8,7 +8,7 @@ V2版只支持`APK Signature Scheme v2`,要求在 `signingConfigs` 里 `v2Sign ## 最新版本 -- **v2.0.0 - 2017.06.16** - 全新发布,支持V2签名模式,包含多项优化 +- **v2.0.0 - 2017.06.30** - 全新发布,支持V2签名模式,包含多项优化 ## 项目介绍 diff --git a/sample/build.gradle b/sample/build.gradle index fe0896c..5321773 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.8.15-SNAPSHOT' + ext.packer_version = '1.9.0-SNAPSHOT' repositories { maven { url '/tmp/repo/' } From 75251c18bd50398307bf9aea1b98ac5ed25d5557 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 9 Jun 2017 17:55:58 +0800 Subject: [PATCH 47/67] add packer-ng-1.9.0.jar --- ...acker-ng-1.8.0.jar => packer-ng-1.9.0.jar} | Bin 223287 -> 227108 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename tools/{packer-ng-1.8.0.jar => packer-ng-1.9.0.jar} (88%) diff --git a/tools/packer-ng-1.8.0.jar b/tools/packer-ng-1.9.0.jar similarity index 88% rename from tools/packer-ng-1.8.0.jar rename to tools/packer-ng-1.9.0.jar index 8c6bcaf6349b6cfae9cc63e42d17bae024aefbcb..eba127b58c902684194531f43a9ac08350a454bd 100644 GIT binary patch delta 17316 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 Date: Mon, 12 Jun 2017 15:30:48 +0800 Subject: [PATCH 48/67] release v1.9.1 for test --- .../com/mcxiaoke/packer/common/KMPReader.java | 5 ++++- .../com/mcxiaoke/packer/common/PackerCommon.java | 6 ------ gradle.properties | 2 +- sample/build.gradle | 2 +- tools/packer-ng-v2.py | 15 +++++++-------- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java b/common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java index c53f37e..1da7728 100644 --- a/common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java +++ b/common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java @@ -17,6 +17,9 @@ class KMPReader { + // zip block size max + public static final int BLOCK_SIZE_MAX = 0x100000; // 1M + public static String readChannel(File file) throws IOException { String payload = readPayload(file); Map values = PackerCommon.mapFromString(payload); @@ -28,7 +31,7 @@ public static String readPayload(File file) throws IOException { FileChannel fc = null; try { long fileSize = file.length(); - long blockSize = PackerCommon.BLOCK_SIZE_MAX; + long blockSize = BLOCK_SIZE_MAX; long offset = Math.max(0, fileSize - blockSize); raf = new RandomAccessFile(file, "r"); fc = raf.getChannel(); diff --git a/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java index b24f463..d594b99 100644 --- a/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java +++ b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java @@ -36,17 +36,11 @@ public class PackerCommon { public static final int CHANNEL_BLOCK_ID = 0x7a786b21; // "zxk!" // channel info key public static final String CHANNEL_KEY = "CHANNEL"; - // zip block size max - public static final int BLOCK_SIZE_MAX = 0x100000; public static String readChannel(File file) throws IOException { return readValue(file, CHANNEL_KEY, CHANNEL_BLOCK_ID); } - static String readChannel2(File file) throws IOException { - return KMPReader.readChannel(file); - } - public static void writeChannel(File file, String channel) throws IOException { writeValue(file, CHANNEL_KEY, channel, CHANNEL_BLOCK_ID); diff --git a/gradle.properties b/gradle.properties index 373f3d0..2894570 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.9.0 +VERSION_NAME=1.9.1 VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/sample/build.gradle b/sample/build.gradle index 5321773..09b84dd 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.9.0-SNAPSHOT' + ext.packer_version = '1.9.1-SNAPSHOT' repositories { maven { url '/tmp/repo/' } diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index 1722eaf..f93cda9 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -2,7 +2,7 @@ # @Author: mcxiaoke # @Date: 2017-06-06 14:03:18 # @Last Modified by: mcxiaoke -# @Last Modified time: 2017-06-09 17:19:52 +# @Last Modified time: 2017-06-12 15:06:25 from __future__ import print_function import os import sys @@ -17,12 +17,12 @@ logger = logging.getLogger(__name__) AUTHOR = 'mcxiaoke' -VERSION = '1.0.0' +VERSION = '2.0.0' try: props = dict(line.strip().split('=') for line in open('../gradle.properties') if line.strip()) VERSION = props.get('VERSION_NAME') except Exception as e: - VERSION = '1.0.0' + VERSION = '2.0.0' ##################################################################### @@ -171,8 +171,7 @@ def createMap(apk): access=mmap.ACCESS_READ) -def findBlock1(apk): - # # search Plugin Magic words +def findBlockByPluginMagic(apk): mm = createMap(apk) magicLen = len(PLUGIN_BLOCK_MAGIC) start = mm.rfind(PLUGIN_BLOCK_MAGIC) @@ -194,14 +193,14 @@ def findBlock1(apk): return block -def findBlock2(apk): +def findBlockBySigningMagic(apk): # search APK Signing Block Magic words signingBlock = findBySigningMagic(apk) if signingBlock: return parseApkSigningBlock(signingBlock, PLUGIN_BLOCK_ID) -def findBlock3(apk): +def findBlockByZipSections(apk): # find zip centralDirectory, then find apkSigningBlock signingBlock = findByZipSections(apk) if signingBlock: @@ -483,7 +482,7 @@ def getChannel(apk): try: zp = zipfile.ZipFile(apk) zp.testzip() - content = findBlock3(apk) + content = findBlockByZipSections(apk) values = parseValues(content) if values: channel = values.get(PLUGIN_CHANNEL_KEY) From cee3cbbed958b73c59948ea507aae70b3ecbe548 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Mon, 12 Jun 2017 16:12:41 +0800 Subject: [PATCH 49/67] update executable jar --- ...acker-ng-1.9.0.jar => packer-ng-1.9.1.jar} | Bin 227108 -> 227100 bytes 1 file changed, 0 insertions(+), 0 deletions(-) rename tools/{packer-ng-1.9.0.jar => packer-ng-1.9.1.jar} (92%) diff --git a/tools/packer-ng-1.9.0.jar b/tools/packer-ng-1.9.1.jar similarity index 92% rename from tools/packer-ng-1.9.0.jar rename to tools/packer-ng-1.9.1.jar index eba127b58c902684194531f43a9ac08350a454bd..80994adcd998d5d180e7a29a0274956aed76744d 100644 GIT binary patch delta 8692 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 SfB7iNfgebX!zTgj5&s9wT7Lup From f53a5619642c9baeb895008c00d68bbcf0ae4901 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Tue, 13 Jun 2017 14:36:11 +0800 Subject: [PATCH 50/67] simplify common and helper classes --- .../com/mcxiaoke/packer/common/KMPMatch.java | 95 ------------------- .../com/mcxiaoke/packer/common/KMPReader.java | 70 -------------- .../mcxiaoke/packer/common/PackerCommon.java | 18 +--- .../packer/support/walle/PayloadReader.java | 8 +- .../packer/support/walle/PayloadWriter.java | 2 +- .../packer/support/walle/Support.java | 35 +++++++ .../mcxiaoke/packer/common/PayloadTests.java | 27 ++---- .../com/mcxiaoke/packer/helper/PackerNg.java | 56 ++--------- 8 files changed, 59 insertions(+), 252 deletions(-) delete mode 100644 common/src/main/java/com/mcxiaoke/packer/common/KMPMatch.java delete mode 100644 common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java create mode 100644 common/src/main/java/com/mcxiaoke/packer/support/walle/Support.java diff --git a/common/src/main/java/com/mcxiaoke/packer/common/KMPMatch.java b/common/src/main/java/com/mcxiaoke/packer/common/KMPMatch.java deleted file mode 100644 index f16f6b5..0000000 --- a/common/src/main/java/com/mcxiaoke/packer/common/KMPMatch.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.mcxiaoke.packer.common; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * https://store.fmi.uni-sofia.bg/fmi/logic/vboutchkova/sources/KMPMatch.java - * User: mcxiaoke - * Date: 2017/6/9 - * Time: 12:21 - */ - -class KMPMatch { - - private byte[] pattern; - private int[] failure; - - public KMPMatch(byte[] pattern) { - this.pattern = pattern; - computeFailure(); - } - - public int find(InputStream is) throws IOException { - int i = 0; - int j = 0; - int b; - while ((b = is.read()) != -1) { - i++; - while (j > 0 && pattern[j] != b) { - j = failure[j - 1]; - } - if (pattern[j] == b) { - j++; - } - if (j == pattern.length) { - return i; - } - } - return -1; - } - - public int find(ByteBuffer buf) { - int j = 0; - int p = buf.position(); - while (buf.hasRemaining()) { - byte b = buf.get(); - while (j > 0 && pattern[j] != b) { - j = failure[j - 1]; - } - if (pattern[j] == b) { - j++; - } - if (j == pattern.length) { - int q = buf.position() - p; - return q - pattern.length + 1; - } - } - return -1; - } - - public int find(byte[] data) { - int j = 0; - if (data.length == 0) return -1; - if (data.length < pattern.length) return -1; - - for (int i = 0; i < data.length; i++) { - while (j > 0 && pattern[j] != data[i]) { - j = failure[j - 1]; - } - if (pattern[j] == data[i]) { - j++; - } - if (j == pattern.length) { - return i - pattern.length + 1; - } - } - return -1; - } - - private void computeFailure() { - failure = new int[pattern.length]; - int j = 0; - for (int i = 1; i < pattern.length; i++) { - while (j > 0 && pattern[j] != pattern[i]) { - j = failure[j - 1]; - } - if (pattern[j] == pattern[i]) { - j++; - } - failure[i] = j; - } - } - -} diff --git a/common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java b/common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java deleted file mode 100644 index 1da7728..0000000 --- a/common/src/main/java/com/mcxiaoke/packer/common/KMPReader.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.mcxiaoke.packer.common; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteOrder; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileChannel.MapMode; -import java.util.Map; - -/** - * User: mcxiaoke - * Date: 2017/6/9 - * Time: 14:47 - */ - -class KMPReader { - - // zip block size max - public static final int BLOCK_SIZE_MAX = 0x100000; // 1M - - public static String readChannel(File file) throws IOException { - String payload = readPayload(file); - Map values = PackerCommon.mapFromString(payload); - return values == null ? null : values.get(PackerCommon.CHANNEL_KEY); - } - - public static String readPayload(File file) throws IOException { - RandomAccessFile raf = null; - FileChannel fc = null; - try { - long fileSize = file.length(); - long blockSize = BLOCK_SIZE_MAX; - long offset = Math.max(0, fileSize - blockSize); - raf = new RandomAccessFile(file, "r"); - fc = raf.getChannel(); - byte[] magic = PackerCommon.BLOCK_MAGIC.getBytes(PackerCommon.UTF8); - MappedByteBuffer buffer = fc.map(MapMode.READ_ONLY, offset, blockSize); - buffer.order(ByteOrder.LITTLE_ENDIAN); - KMPMatch kmp = new KMPMatch(magic); - int index = kmp.find(buffer); - if (index < 0) { - return null; - } -// System.out.println("index=" + index + " offset=" -// + (size - blockSize + index)); - byte[] actual = new byte[magic.length]; - buffer.position(index - 1); - buffer.get(actual); -// System.out.println("actual=" + new String(actual, "UTF-8")); - int len = buffer.getInt(); -// System.out.println("payload length=" + len); - if (len < 0 || len > blockSize) { - return null; - } - byte[] payload = new byte[len]; - buffer.get(payload); -// System.out.println("payload=" + payloadStr); - return new String(payload, PackerCommon.UTF8); - } finally { - if (fc != null) { - fc.close(); - } - if (raf != null) { - raf.close(); - } - } - } -} diff --git a/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java index d594b99..9030e9e 100644 --- a/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java +++ b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java @@ -1,19 +1,14 @@ package com.mcxiaoke.packer.common; -import com.mcxiaoke.packer.support.walle.PayloadReader; -import com.mcxiaoke.packer.support.walle.PayloadWriter; +import com.mcxiaoke.packer.support.walle.Support; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.util.Arrays; -import java.util.Date; import java.util.HashMap; -import java.util.Locale; import java.util.Map; import java.util.Map.Entry; @@ -27,8 +22,6 @@ public class PackerCommon { public static final String SEP_LINE = "∙";//\u2219 // charset utf8 public static final String UTF8 = "UTF-8"; - // date string format - private static final String DATE_FORMAT = "yyyy/MM/dd HH:mm:ss Z"; // plugin block magic public static final String BLOCK_MAGIC = "Packer Ng Sig V2"; // magic @@ -125,13 +118,13 @@ static void writePayloadImpl(File file, int blockId) throws IOException { ByteBuffer buffer = wrapPayload(payload); - PayloadWriter.writeBlock(file, blockId, buffer); + Support.writeBlock(file, blockId, buffer); } // package visible for test static byte[] readPayloadImpl(File file, int blockId) throws IOException { - ByteBuffer buffer = PayloadReader.readBlockBuffer(file, blockId); + ByteBuffer buffer = Support.readBlock(file, blockId); if (buffer == null) { return null; } @@ -202,9 +195,4 @@ public static Map mapFromString(final String string) { return map; } - // package visible for test - static String getDateString() { - final DateFormat df = new SimpleDateFormat(DATE_FORMAT, Locale.US); - return df.format(new Date()); - } } diff --git a/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadReader.java b/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadReader.java index 126e2eb..73551c0 100644 --- a/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadReader.java +++ b/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadReader.java @@ -7,18 +7,18 @@ import java.nio.channels.FileChannel; import java.util.Map; -public final class PayloadReader { +final class PayloadReader { private PayloadReader() { super(); } - public static byte[] readBlock(final File apkFile, final int id) + public static byte[] readBytes(final File apkFile, final int id) throws IOException { - final ByteBuffer buf = readBlockBuffer(apkFile, id); + final ByteBuffer buf = readBlock(apkFile, id); return buf == null ? null : V2Utils.getBytes(buf); } - public static ByteBuffer readBlockBuffer(final File apkFile, final int id) + public static ByteBuffer readBlock(final File apkFile, final int id) throws IOException { final Map blocks = readAllBlocks(apkFile); if (blocks == null) { diff --git a/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadWriter.java b/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadWriter.java index 09916d1..85703ec 100644 --- a/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadWriter.java +++ b/common/src/main/java/com/mcxiaoke/packer/support/walle/PayloadWriter.java @@ -11,7 +11,7 @@ import java.util.Set; -public final class PayloadWriter { +final class PayloadWriter { private PayloadWriter() { super(); } diff --git a/common/src/main/java/com/mcxiaoke/packer/support/walle/Support.java b/common/src/main/java/com/mcxiaoke/packer/support/walle/Support.java new file mode 100644 index 0000000..75f1901 --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/support/walle/Support.java @@ -0,0 +1,35 @@ +package com.mcxiaoke.packer.support.walle; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * bridge class between support and common + * User: mcxiaoke + * Date: 2017/6/13 + * Time: 14:06 + */ + +public class Support { + + public static ByteBuffer readBlock(final File apkFile, final int id) + throws IOException { + return PayloadReader.readBlock(apkFile, id); + } + + public static byte[] readBytes(final File apkFile, final int id) + throws IOException { + return PayloadReader.readBytes(apkFile, id); + } + + public static void writeBlock(final File apkFile, final int id, + final ByteBuffer buffer) throws IOException { + PayloadWriter.writeBlock(apkFile, id, buffer); + } + + public static void writeBlock(final File apkFile, final int id, + final byte[] bytes) throws IOException { + PayloadWriter.writeBlock(apkFile, id, bytes); + } +} diff --git a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java index 4ae64a4..f8c2936 100644 --- a/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java +++ b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java @@ -5,8 +5,7 @@ import com.android.apksig.ApkVerifier.IssueWithParams; import com.android.apksig.ApkVerifier.Result; import com.android.apksig.apk.ApkFormatException; -import com.mcxiaoke.packer.support.walle.PayloadReader; -import com.mcxiaoke.packer.support.walle.PayloadWriter; +import com.mcxiaoke.packer.support.walle.Support; import junit.framework.TestCase; import java.io.File; @@ -96,8 +95,8 @@ public void testOverrideSignature() throws IOException, public void testBytesWrite1() throws IOException { File f = newTestFile(); byte[] in = "Hello".getBytes(); - PayloadWriter.writeBlock(f, 0x12345, in); - byte[] out = PayloadReader.readBlock(f, 0x12345); + Support.writeBlock(f, 0x12345, in); + byte[] out = Support.readBytes(f, 0x12345); assertTrue(TestUtils.sameBytes(in, out)); checkApkVerified(f); } @@ -105,8 +104,8 @@ public void testBytesWrite1() throws IOException { public void testBytesWrite2() throws IOException { File f = newTestFile(); byte[] in = "中文和特殊符号测试!@#¥%……*()《》?:【】、".getBytes("UTF-8"); - PayloadWriter.writeBlock(f, 0x12345, in); - byte[] out = PayloadReader.readBlock(f, 0x12345); + Support.writeBlock(f, 0x12345, in); + byte[] out = Support.readBytes(f, 0x12345); assertTrue(TestUtils.sameBytes(in, out)); checkApkVerified(f); } @@ -199,8 +198,8 @@ public void testBufferWrite() throws IOException { in.put(string); in.flip(); // important // TestUtils.showBuffer(in); - PayloadWriter.writeBlock(f, 0x123456, in); - ByteBuffer out = PayloadReader.readBlockBuffer(f, 0x123456); + Support.writeBlock(f, 0x123456, in); + ByteBuffer out = Support.readBlock(f, 0x123456); assertNotNull(out); // TestUtils.showBuffer(out); assertEquals(123, out.getInt()); @@ -227,16 +226,4 @@ public void testChannelWriteRead() throws IOException { checkApkVerified(f); } - public void testKMPReader() throws IOException { - File f = newTestFile(); - PackerCommon.writeChannel(f, "Hello2"); - assertEquals("Hello2", PackerCommon.readChannel2(f)); - PackerCommon.writeChannel(f, "中文@#$222"); - assertEquals("中文@#$222", PackerCommon.readChannel2(f)); - PackerCommon.writeChannel(f, "中文 C2222"); - assertEquals("中文 C2222", PackerCommon.readChannel2(f)); - checkApkVerified(f); - } - - } diff --git a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java index 3096dbc..19b7a6f 100644 --- a/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java +++ b/helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java @@ -17,64 +17,26 @@ public final class PackerNg { private static final String EMPTY_STRING = ""; private static String sCachedChannel; - public static String readChannel(final File file) { + public static String getChannel(final File file) { try { return PackerCommon.readChannel(file); - } catch (IOException e) { + } catch (Exception e) { return EMPTY_STRING; } } public static String getChannel(final Context context) { - return getChannel(context, EMPTY_STRING); - } - - public static synchronized String getChannel(final Context context, - final String defValue) { - if (sCachedChannel == null) { - sCachedChannel = getMarketInternal(context, defValue).channel; - } - return sCachedChannel; - } - - public static ChannelInfo getChannelInfo(final Context context) { - return getChannelInfo(context, EMPTY_STRING); - } - - public static synchronized ChannelInfo getChannelInfo(final Context context, - final String defValue) { - return getMarketInternal(context, defValue); - } - - private static ChannelInfo getMarketInternal(final Context context, - final String defValue) { - String market = null; - Exception error = null; try { - final ApplicationInfo info = context.getApplicationInfo(); - final File apkFile = new File(info.sourceDir); - market = PackerCommon.readChannel(apkFile); + return getChannelOrThrow(context); } catch (Exception e) { - error = e; + return EMPTY_STRING; } - return new ChannelInfo(market == null ? defValue : market, error); } - public static final class ChannelInfo { - public final String channel; - public final Exception error; - - public ChannelInfo(final String channel, final Exception error) { - this.channel = channel; - this.error = error; - } - - @Override - public String toString() { - return "ChannelInfo{" + - "market='" + channel + '\'' + - ", error=" + error + - '}'; - } + public static synchronized String getChannelOrThrow(final Context context) + throws IOException { + final ApplicationInfo info = context.getApplicationInfo(); + return PackerCommon.readChannel(new File(info.sourceDir)); } + } From 1d52c269aae9d668f0da8e013f6426846ae644ad Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Tue, 13 Jun 2017 14:36:26 +0800 Subject: [PATCH 51/67] release v1.9.2 for test --- gradle.properties | 2 +- sample/build.gradle | 2 +- .../src/main/java/com/mcxiaoke/packer/samples/MainActivity.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2894570..cfa5674 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.9.1 +VERSION_NAME=1.9.2 VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/sample/build.gradle b/sample/build.gradle index 09b84dd..f91e1f2 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.9.1-SNAPSHOT' + ext.packer_version = '1.9.2-SNAPSHOT' repositories { maven { url '/tmp/repo/' } diff --git a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java index 78df37a..2cebf08 100644 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java +++ b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java @@ -34,7 +34,7 @@ protected void onCreate(Bundle savedInstanceState) { for (ApplicationInfo app : apps) { if (app.packageName.startsWith("com.douban.")) { Log.d("TAG", "app=" + app.packageName + ", channel=" - + PackerNg.readChannel(new File(app.sourceDir))); + + PackerNg.getChannel(new File(app.sourceDir))); } } From b5bee4aa2364f8e0af5d00005ecf8d95d824b579 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Tue, 13 Jun 2017 15:27:43 +0800 Subject: [PATCH 52/67] add v1.9.2 executable jar --- .gitignore | 4 ++++ ...acker-ng-1.9.1.jar => packer-ng-1.9.2.jar} | Bin 227100 -> 224452 bytes tools/packer-ng-v2.py | 3 +++ 3 files changed, 7 insertions(+) rename tools/{packer-ng-1.9.1.jar => packer-ng-1.9.2.jar} (90%) diff --git a/.gitignore b/.gitignore index 83afda7..a540234 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,8 @@ channels/ *.iml *.apk *.pyc +*.class .DS_Store +.classpath +.project +.settings/ diff --git a/tools/packer-ng-1.9.1.jar b/tools/packer-ng-1.9.2.jar similarity index 90% rename from tools/packer-ng-1.9.1.jar rename to tools/packer-ng-1.9.2.jar index 80994adcd998d5d180e7a29a0274956aed76744d..6d4c2d68f5b2d06f4b73164637cea66a075aa322 100644 GIT binary patch delta 10184 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 OjAU613^q|nhw*;}$UQg! delta 12764 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 H_y7L@CPoV7 diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index f93cda9..9f46a45 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -62,14 +62,17 @@ class ZipFormatException(Exception): + '''ZipFormatException''' pass class SignatureNotFoundException(Exception): + '''SignatureNotFoundException''' pass class MagicNotFoundException(Exception): + '''MagicNotFoundException''' pass ##################################################################### From 167ede1c967c271cf8b676cbdcf180dff5a83289 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Tue, 13 Jun 2017 18:23:25 +0800 Subject: [PATCH 53/67] format python script --- tools/packer-ng-v2.py | 58 ++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index 9f46a45..7cfbfb3 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -13,13 +13,14 @@ import time logging.basicConfig(format='%(levelname)s:%(lineno)s: %(funcName)s() %(message)s', - level=logging.ERROR) + level=logging.DEBUG) logger = logging.getLogger(__name__) AUTHOR = 'mcxiaoke' VERSION = '2.0.0' try: - props = dict(line.strip().split('=') for line in open('../gradle.properties') if line.strip()) + props = dict(line.strip().split('=') for line in + open('../gradle.properties') if line.strip()) VERSION = props.get('VERSION_NAME') except Exception as e: VERSION = '2.0.0' @@ -89,31 +90,31 @@ def __init__(self, buf, littleEndian=True): self.sign = '<' if littleEndian else '>' def getShort(self, offset=0): - return struct.unpack('{}h'.format(self.sign), self.buf[offset:offset+2])[0] + return struct.unpack('{}h'.format(self.sign), self.buf[offset:offset + 2])[0] def getUShort(self, offset=0): - return struct.unpack('{}H'.format(self.sign), self.buf[offset:offset+2])[0] + return struct.unpack('{}H'.format(self.sign), self.buf[offset:offset + 2])[0] def getInt(self, offset=0): - return struct.unpack('{}i'.format(self.sign), self.buf[offset:offset+4])[0] + return struct.unpack('{}i'.format(self.sign), self.buf[offset:offset + 4])[0] def getUInt(self, offset=0): - return struct.unpack('{}I'.format(self.sign), self.buf[offset:offset+4])[0] + return struct.unpack('{}I'.format(self.sign), self.buf[offset:offset + 4])[0] def getLong(self, offset=0): - return struct.unpack('{}q'.format(self.sign), self.buf[offset:offset+8])[0] + return struct.unpack('{}q'.format(self.sign), self.buf[offset:offset + 8])[0] def getULong(self, offset=0): - return struct.unpack('{}Q'.format(self.sign), self.buf[offset:offset+8])[0] + return struct.unpack('{}Q'.format(self.sign), self.buf[offset:offset + 8])[0] def getFloat(self, offset=0): - return struct.unpack('{}f'.format(self.sign), self.buf[offset:offset+4])[0] + return struct.unpack('{}f'.format(self.sign), self.buf[offset:offset + 4])[0] def getDouble(self, offset=0): - return struct.unpack('{}d'.format(self.sign), self.buf[offset:offset+8])[0] + return struct.unpack('{}d'.format(self.sign), self.buf[offset:offset + 8])[0] def getChars(self, offset=0, size=16): - return struct.unpack('{}{}'.format(self.sign, 's'*size), self.buf[offset:offset+size]) + return struct.unpack('{}{}'.format(self.sign, 's' * size), self.buf[offset:offset + size]) ##################################################################### @@ -189,7 +190,7 @@ def findBlockByPluginMagic(apk): end = start + magicLen + 4 + payloadLen + 4 logger.debug('magic end offset=%s', end) - logger.debug('magic payloadLen2=%s', d.getInt(end-4)) + logger.debug('magic payloadLen2=%s', d.getInt(end - 4)) block = mm[start:end] mm.close() @@ -222,7 +223,7 @@ def findBySigningMagic(apk): logger.debug('magic string=%s', ''.join(d.getChars(index, 16))) bEnd = index + 16 logger.debug('block end=%s', bEnd) - bSize = d.getLong(bEnd-24)+8 + bSize = d.getLong(bEnd - 24) + 8 logger.debug('block size=%s', bSize) bStart = bEnd - bSize logger.debug('block start=%s', bStart) @@ -253,7 +254,7 @@ def findByZipSections(apk): "APK too small for APK Signing Block. ZIP Central Directory offset: " + centralDirStartOffset) - fStart = centralDirStartOffset-24 + fStart = centralDirStartOffset - 24 mStart = centralDirStartOffset - 16 fEnd = centralDirStartOffset logger.debug('fStart:%s', fStart) @@ -294,7 +295,7 @@ def findByZipSections(apk): raise SignatureNotFoundException( "APK Signing Block offset out of range: " + apkSigBlockOffset) - apkSigBlock = mm[apkSigBlockOffset:apkSigBlockOffset+8] + apkSigBlock = mm[apkSigBlockOffset:apkSigBlockOffset + 8] # logger.debug('apkSigBlock:%s', to_hex(apkSigBlock)) apkSigBlockSizeInHeader = ByteDecoder(apkSigBlock).getLong(0) logger.debug('apkSigBlockSizeInHeader:%s', apkSigBlockSizeInHeader) @@ -304,7 +305,7 @@ def findByZipSections(apk): "APK Signing Block sizes in header and footer do not match: " + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter) - block = mm[apkSigBlockOffset:apkSigBlockOffset+totalSize] + block = mm[apkSigBlockOffset:apkSigBlockOffset + totalSize] mm.close() return block @@ -326,7 +327,7 @@ def parseApkSigningBlock(block, blockId): bd0 = ByteDecoder(block) blockSizeInHeader = bd0.getULong(0) logger.debug('blockSizeInHeader:%s', blockSizeInHeader) - blockSizeInFooter = bd0.getULong(totalSize-24) + blockSizeInFooter = bd0.getULong(totalSize - 24) logger.debug('blockSizeInFooter:%s', blockSizeInFooter) # slice only payload block = block[8:-24] @@ -362,13 +363,13 @@ def parseApkSigningBlock(block, blockId): position += 4 if sid == APK_SIGNATURE_SCHEME_V2_BLOCK_ID: logger.debug('found signingBlock') - signingBlock = block[position:position+lenLong-4] + signingBlock = block[position:position + lenLong - 4] signingBlockSize = len(signingBlock) logger.debug('signingBlockSize:%s', signingBlockSize) # logger.debug('signingBlockHex:%s', to_hex(signingBlock[0:32])) elif sid == blockId: logger.debug('found pluginBlock') - pluginBlock = block[position:position+lenLong-4] + pluginBlock = block[position:position + lenLong - 4] pluginBlockSize = len(pluginBlock) logger.debug('pluginBlockSize:%s', pluginBlockSize) logger.debug('pluginBlock:%s', pluginBlock) @@ -402,7 +403,8 @@ def findZipSections(mm): "ZIP Central Directory overlaps with End of Central Directory" + ". CD end: " + cdEndOffset + ", EoCD start: " + eocdOffset) - cdRecordCount = ed.getUShort(ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET) + cdRecordCount = ed.getUShort( + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET) logger.debug('cdRecordCount:%s', cdRecordCount) sections = ZipSections(cdStartOffset, cdSizeBytes, @@ -425,21 +427,23 @@ def findEocdRecord(mm): logger.debug('maxEocdSize:%s', maxEocdSize) bufOffsetInFile = fileSize - maxEocdSize logger.debug('bufOffsetInFile:%s', bufOffsetInFile) - buf = mm[bufOffsetInFile:bufOffsetInFile+maxEocdSize] + buf = mm[bufOffsetInFile:bufOffsetInFile + maxEocdSize] # logger.debug('buf:%s',to_hex(buf)) eocdOffsetInBuf = findEocdStartOffset(buf) logger.debug('eocdOffsetInBuf:%s', eocdOffsetInBuf) if eocdOffsetInBuf != -1: - return bufOffsetInFile+eocdOffsetInBuf, buf[eocdOffsetInBuf:] + return bufOffsetInFile + eocdOffsetInBuf, buf[eocdOffsetInBuf:] def findEocdStartOffset(buf): archiveSize = len(buf) logger.debug('archiveSize:%s', archiveSize) - maxCommentLength = min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE) + maxCommentLength = min( + archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE) logger.debug('maxCommentLength:%s', maxCommentLength) eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE - logger.debug('eocdWithEmptyCommentStartPosition:%s', eocdWithEmptyCommentStartPosition) + logger.debug('eocdWithEmptyCommentStartPosition:%s', + eocdWithEmptyCommentStartPosition) expectedCommentLength = 0 eocdOffsetInBuf = -1 while expectedCommentLength <= maxCommentLength: @@ -450,7 +454,8 @@ def findEocdStartOffset(buf): seg = ByteDecoder(buf).getInt(eocdStartPos) logger.debug('seg:%s', hex(seg)) if seg == ZIP_EOCD_REC_SIG: - actualCommentLength = ByteDecoder(buf).getUShort(eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET) + actualCommentLength = ByteDecoder(buf).getUShort( + eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET) logger.debug('actualCommentLength:%s', actualCommentLength) if actualCommentLength == expectedCommentLength: logger.debug('found eocdStartPos:%s', eocdStartPos) @@ -485,7 +490,7 @@ def getChannel(apk): try: zp = zipfile.ZipFile(apk) zp.testzip() - content = findBlockByZipSections(apk) + content = findBlockByPluginMagic(apk) values = parseValues(content) if values: channel = values.get(PLUGIN_CHANNEL_KEY) @@ -522,5 +527,6 @@ def main(): print('Channel: \t{}'.format(channel)) showInfo(apk) + if __name__ == '__main__': main() From 5a31c85d343b5625faa7f83c78fe5f394abd20e4 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Tue, 13 Jun 2017 18:24:44 +0800 Subject: [PATCH 54/67] add read channel c code --- .gitignore | 1 + tools/read.c | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 tools/read.c diff --git a/.gitignore b/.gitignore index a540234..440f742 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ channels/ *.pyc *.class .DS_Store +a.out .classpath .project .settings/ diff --git a/tools/read.c b/tools/read.c new file mode 100644 index 0000000..07f7c48 --- /dev/null +++ b/tools/read.c @@ -0,0 +1,169 @@ +/* + * @Author: mcxiaoke + * @Date: 2017-06-13 15:47:02 + * @Last Modified by: mcxiaoke + * @Last Modified time: 2017-06-13 18:23:41 + */ +#include +#include +#include +#include +#include +#include +#include + +/* + * http://man7.org/linux/man-pages/man2/mmap.2.html + * https://en.wikipedia.org/wiki/Mmap + */ + +static const size_t block_size = 0x100000; +static const char *sep_kv = "∘"; +static const char *sep_line = "∙"; +static const char *magic = "Packer Ng Sig V2"; +static const char *key = "CHANNEL"; + +#define handle_error(msg) \ + do \ + { \ + perror(msg); \ + exit(EXIT_FAILURE); \ + } while (0) + +/* find the overlap array for the given pattern */ +void find_overlap(const char *word, int wlen, int *ptr) +{ + int i = 2, j = 0, len = wlen; + + ptr[0] = -1; + ptr[1] = 0; + + while (i < len) + { + if (word[i - 1] == word[j]) + { + j = j + 1; + ptr[i] = j; + i = i + 1; + } + else if (j > 0) + { + j = ptr[j]; + } + else + { + ptr[i] = 0; + i = i + 1; + } + } + return; +} + +/* +* finds the position of the pattern in the given target string +* target - str, patter - word +*/ +int kmp_search(const char *str, int slen, const char *word, int wlen) +{ + int *ptr = (int *)calloc(1, sizeof(int) * (strlen(magic))); + find_overlap(magic, strlen(magic), ptr); + int i = 0, j = 0; + + while ((i + j) < slen) + { + /* match found on the target and pattern string char */ + if (word[j] == str[i + j]) + { + if (j == (wlen - 1)) + { + // printf("%s located at the index %d\n", word, i + 1); + return i + 1; + } + j = j + 1; + } + else + { + /* manipulating next indices to compare */ + i = i + j - ptr[j]; + if (ptr[j] > -1) + { + j = ptr[j]; + } + else + { + j = 0; + } + } + } + return -1; +} + +int main(int argc, char *argv[]) +{ + char *addr; + int fd; + struct stat sb; + off_t offset, pa_offset; + size_t length; + ssize_t s; + + if (argc < 2) + { + fprintf(stderr, "%s file\n", argv[0]); + exit(EXIT_FAILURE); + } + + fd = open(argv[1], O_RDONLY); + if (fd == -1) + handle_error("open"); + + if (fstat(fd, &sb) == -1) /* To obtain file siclearze */ + handle_error("fstat"); + + offset = sb.st_size - block_size; + pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1); + /* offset for mmap() must be page aligned */ + length = sb.st_size - offset; + printf("mmap file size=%zu\n", length); + size_t pa_length = length + offset - pa_offset; + printf("mmap real size=%zu\n", pa_length); + printf("mmap real offset=%lld\n", pa_offset); + addr = mmap(NULL, pa_length, PROT_READ, + MAP_PRIVATE, fd, pa_offset); + if (addr == MAP_FAILED) + handle_error("mmap"); + + // s = write(STDOUT_FILENO, addr + offset - pa_offset, length); + // if (s != length) + // { + // if (s == -1) + // handle_error("write"); + + // fprintf(stderr, "partial write"); + // exit(EXIT_FAILURE); + // } + + size_t index = kmp_search(addr, pa_length, magic, strlen(magic)); + printf("magic index=%zu\n", index); + size_t li = index + strlen(magic) - 1; + printf("magic lenindex=%zu\n", li); + int32_t payload_len; + memcpy(&payload_len, &addr[li], 4); + printf("payload_len=%d\n", payload_len); + char *payload = malloc(payload_len + 1); + strncpy(payload, &addr[li + 4], payload_len); + printf("payload=%s\n", payload); + char *pos1 = strstr(payload, sep_kv); + char *pos2 = strstr(payload, sep_line); + size_t n1 = pos1 - payload + strlen(sep_kv); + size_t n2 = pos2 - payload; + size_t clen = n2 - n1; + printf("n1=%zu, n2=%zu, clen=%zu\n", n1, n2, clen); + char *channel = malloc(clen); + strncpy(channel, &payload[n1], clen); + printf("channel=%s\n", channel); + munmap(addr, pa_length); + close(fd); + + exit(EXIT_SUCCESS); +} \ No newline at end of file From 7da520d5206efcad1c25cb84065286d9dd4c69ad Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 14 Jun 2017 11:37:42 +0800 Subject: [PATCH 55/67] update c read code --- tools/packer-ng-v2.py | 2 +- tools/read.c | 259 ++++++++++++++++++++---------------------- 2 files changed, 125 insertions(+), 136 deletions(-) diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index 7cfbfb3..79f3aaf 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -13,7 +13,7 @@ import time logging.basicConfig(format='%(levelname)s:%(lineno)s: %(funcName)s() %(message)s', - level=logging.DEBUG) + level=logging.ERROR) logger = logging.getLogger(__name__) AUTHOR = 'mcxiaoke' diff --git a/tools/read.c b/tools/read.c index 07f7c48..417ccad 100644 --- a/tools/read.c +++ b/tools/read.c @@ -1,15 +1,15 @@ /* - * @Author: mcxiaoke - * @Date: 2017-06-13 15:47:02 + * @Author: mcxiaoke + * @Date: 2017-06-13 15:47:02 * @Last Modified by: mcxiaoke * @Last Modified time: 2017-06-13 18:23:41 */ -#include -#include #include -#include #include #include +#include +#include +#include #include /* @@ -23,147 +23,136 @@ static const char *sep_line = "∙"; static const char *magic = "Packer Ng Sig V2"; static const char *key = "CHANNEL"; -#define handle_error(msg) \ - do \ - { \ - perror(msg); \ - exit(EXIT_FAILURE); \ - } while (0) - -/* find the overlap array for the given pattern */ -void find_overlap(const char *word, int wlen, int *ptr) -{ - int i = 2, j = 0, len = wlen; +#define handle_error(msg) \ + do { \ + perror(msg); \ + exit(EXIT_FAILURE); \ + } while (0) - ptr[0] = -1; - ptr[1] = 0; +#define handle_not_found() \ + do { \ + printf("\n"); \ + exit(EXIT_FAILURE); \ + } while (0) - while (i < len) - { - if (word[i - 1] == word[j]) - { - j = j + 1; - ptr[i] = j; - i = i + 1; - } - else if (j > 0) - { - j = ptr[j]; - } - else - { - ptr[i] = 0; - i = i + 1; - } +/* find the overlap array for the given pattern */ +void find_overlap(const char *word, size_t wlen, int *ptr) { + size_t i = 2, j = 0, len = wlen; + ptr[0] = -1; + ptr[1] = 0; + + while (i < len) { + if (word[i - 1] == word[j]) { + j = j + 1; + ptr[i] = j; + i = i + 1; + } else if (j > 0) { + j = ptr[j]; + } else { + ptr[i] = 0; + i = i + 1; } - return; + } + return; } /* * finds the position of the pattern in the given target string * target - str, patter - word */ -int kmp_search(const char *str, int slen, const char *word, int wlen) -{ - int *ptr = (int *)calloc(1, sizeof(int) * (strlen(magic))); - find_overlap(magic, strlen(magic), ptr); - int i = 0, j = 0; - - while ((i + j) < slen) - { - /* match found on the target and pattern string char */ - if (word[j] == str[i + j]) - { - if (j == (wlen - 1)) - { - // printf("%s located at the index %d\n", word, i + 1); - return i + 1; - } - j = j + 1; - } - else - { - /* manipulating next indices to compare */ - i = i + j - ptr[j]; - if (ptr[j] > -1) - { - j = ptr[j]; - } - else - { - j = 0; - } - } +int32_t kmp_search(const char *str, size_t slen, const char *word, + size_t wlen) { + // printf("kmp_search() slen=%zu, wlen=%zu\n", slen, wlen); + int *ptr = (int *)calloc(1, sizeof(int) * (strlen(magic))); + find_overlap(magic, strlen(magic), ptr); + int32_t i = 0, j = 0; + + while ((i + j) < slen) { + /* match found on the target and pattern string char */ + if (word[j] == str[i + j]) { + if (j == (wlen - 1)) { + return i + 1; + } + j = j + 1; + } else { + /* manipulating next indices to compare */ + i = i + j - ptr[j]; + if (ptr[j] > -1) { + j = ptr[j]; + } else { + j = 0; + } } - return -1; + } + return -1; } -int main(int argc, char *argv[]) -{ - char *addr; - int fd; - struct stat sb; - off_t offset, pa_offset; - size_t length; - ssize_t s; - - if (argc < 2) - { - fprintf(stderr, "%s file\n", argv[0]); - exit(EXIT_FAILURE); - } - - fd = open(argv[1], O_RDONLY); - if (fd == -1) - handle_error("open"); - - if (fstat(fd, &sb) == -1) /* To obtain file siclearze */ - handle_error("fstat"); - +int main(int argc, char *argv[]) { + char *addr; + int fd; + struct stat sb; + off_t offset, pa_offset; + size_t length; + ssize_t s; + + if (argc < 2) { + fprintf(stderr, "%s file\n", argv[0]); + exit(EXIT_FAILURE); + } + fd = open(argv[1], O_RDONLY); + if (fd == -1) + handle_error("open"); + + if (fstat(fd, &sb) == -1) /* To obtain file siclearze */ + handle_error("fstat"); + if (sb.st_size < block_size) { + offset = 0; + } else { offset = sb.st_size - block_size; - pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1); - /* offset for mmap() must be page aligned */ - length = sb.st_size - offset; - printf("mmap file size=%zu\n", length); - size_t pa_length = length + offset - pa_offset; - printf("mmap real size=%zu\n", pa_length); - printf("mmap real offset=%lld\n", pa_offset); - addr = mmap(NULL, pa_length, PROT_READ, - MAP_PRIVATE, fd, pa_offset); - if (addr == MAP_FAILED) - handle_error("mmap"); - - // s = write(STDOUT_FILENO, addr + offset - pa_offset, length); - // if (s != length) - // { - // if (s == -1) - // handle_error("write"); - - // fprintf(stderr, "partial write"); - // exit(EXIT_FAILURE); - // } - - size_t index = kmp_search(addr, pa_length, magic, strlen(magic)); - printf("magic index=%zu\n", index); - size_t li = index + strlen(magic) - 1; - printf("magic lenindex=%zu\n", li); - int32_t payload_len; - memcpy(&payload_len, &addr[li], 4); - printf("payload_len=%d\n", payload_len); - char *payload = malloc(payload_len + 1); - strncpy(payload, &addr[li + 4], payload_len); - printf("payload=%s\n", payload); - char *pos1 = strstr(payload, sep_kv); - char *pos2 = strstr(payload, sep_line); - size_t n1 = pos1 - payload + strlen(sep_kv); - size_t n2 = pos2 - payload; - size_t clen = n2 - n1; - printf("n1=%zu, n2=%zu, clen=%zu\n", n1, n2, clen); - char *channel = malloc(clen); - strncpy(channel, &payload[n1], clen); - printf("channel=%s\n", channel); - munmap(addr, pa_length); - close(fd); - - exit(EXIT_SUCCESS); + } + pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1); + /* offset for mmap() must be page aligned */ + length = sb.st_size - offset; + // printf("mmap file size=%zu\n", length); + size_t pa_length = length + offset - pa_offset; + // printf("mmap real size=%zu\n", pa_length); + // printf("mmap real offset=%lld\n", pa_offset); + addr = mmap(NULL, pa_length, PROT_READ, MAP_PRIVATE, fd, pa_offset); + if (addr == MAP_FAILED) + handle_error("mmap"); + + int32_t index = kmp_search(addr, pa_length, magic, strlen(magic)); + if (index == -1) { + handle_not_found(); + } + // printf("magic index=%d\n", index); + int32_t li = index + strlen(magic) - 1; + // printf("magic lenindex=%d\n", li); + int32_t payload_len; + memcpy(&payload_len, &addr[li], 4); + // printf("payload_len=%d\n", payload_len); + if (payload_len < 0 || payload_len > block_size) { + handle_not_found(); + } + char *payload = malloc(payload_len + 1); + strncpy(payload, &addr[li + 4], payload_len); + // printf("payload=%s\n", payload); + char *pos1 = strstr(payload, sep_kv); + char *pos2 = strstr(payload, sep_line); + if (pos1 == NULL || pos2 == NULL) { + handle_not_found(); + } + size_t n1 = pos1 - payload + strlen(sep_kv); + size_t n2 = pos2 - payload; + size_t clen = n2 - n1; + // printf("n1=%zu, n2=%zu, clen=%zu\n", n1, n2, clen); + char *channel = malloc(clen); + strncpy(channel, &payload[n1], clen); + printf("%s\n", channel); + free(payload); + free(channel); + munmap(addr, pa_length); + close(fd); + + exit(EXIT_SUCCESS); } \ No newline at end of file From dde9a36768bfd5e0869e7db26e3ad1c4c99ec71d Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 14 Jun 2017 14:09:19 +0800 Subject: [PATCH 56/67] fix read.c compile warnings --- tools/read.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tools/read.c b/tools/read.c index 417ccad..1573a50 100644 --- a/tools/read.c +++ b/tools/read.c @@ -17,11 +17,11 @@ * https://en.wikipedia.org/wiki/Mmap */ -static const size_t block_size = 0x100000; +static const off_t block_size = 0x100000; static const char *sep_kv = "∘"; static const char *sep_line = "∙"; static const char *magic = "Packer Ng Sig V2"; -static const char *key = "CHANNEL"; +// static const char *key = "CHANNEL"; #define handle_error(msg) \ do { \ @@ -31,7 +31,7 @@ static const char *key = "CHANNEL"; #define handle_not_found() \ do { \ - printf("\n"); \ + printf("Channel not found.\n"); \ exit(EXIT_FAILURE); \ } while (0) @@ -60,8 +60,7 @@ void find_overlap(const char *word, size_t wlen, int *ptr) { * finds the position of the pattern in the given target string * target - str, patter - word */ -int32_t kmp_search(const char *str, size_t slen, const char *word, - size_t wlen) { +int32_t kmp_search(const char *str, int slen, const char *word, int wlen) { // printf("kmp_search() slen=%zu, wlen=%zu\n", slen, wlen); int *ptr = (int *)calloc(1, sizeof(int) * (strlen(magic))); find_overlap(magic, strlen(magic), ptr); @@ -93,10 +92,10 @@ int main(int argc, char *argv[]) { struct stat sb; off_t offset, pa_offset; size_t length; - ssize_t s; if (argc < 2) { - fprintf(stderr, "%s file\n", argv[0]); + fprintf(stderr, "Usage: %s app.apk - show channel of provided apk\n", + argv[0]); exit(EXIT_FAILURE); } fd = open(argv[1], O_RDONLY); @@ -148,7 +147,7 @@ int main(int argc, char *argv[]) { // printf("n1=%zu, n2=%zu, clen=%zu\n", n1, n2, clen); char *channel = malloc(clen); strncpy(channel, &payload[n1], clen); - printf("%s\n", channel); + printf("Channel: %s\n", channel); free(payload); free(channel); munmap(addr, pa_length); From 2c1d52be526fb66a790a6565441d781dbb53bdc4 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 14 Jun 2017 14:09:30 +0800 Subject: [PATCH 57/67] add generic make file --- tools/Makefile | 223 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 tools/Makefile diff --git a/tools/Makefile b/tools/Makefile new file mode 100644 index 0000000..5a3075d --- /dev/null +++ b/tools/Makefile @@ -0,0 +1,223 @@ +# for macos version compability +export MACOSX_DEPLOYMENT_TARGET = 10.10 + +#### PROJECT SETTINGS #### +# The name of the executable to be created +BIN_NAME := packer +# Compiler used +CC ?= gcc +# Extension of source files used in the project +SRC_EXT = c +# Path to the source directory, relative to the makefile +SRC_PATH = . +# Space-separated pkg-config libraries used by this project +LIBS = +# General compiler flags +COMPILE_FLAGS = -std=c99 -Wall -Wextra -g +# Additional release-specific flags +RCOMPILE_FLAGS = -D NDEBUG +# Additional debug-specific flags +DCOMPILE_FLAGS = -D DEBUG +# Add additional include paths +INCLUDES = -I $(SRC_PATH) +# General linker settings +LINK_FLAGS = +# Additional release-specific linker settings +RLINK_FLAGS = +# Additional debug-specific linker settings +DLINK_FLAGS = +# Destination directory, like a jail or mounted system +DESTDIR = / +# Install path (bin/ is appended automatically) +INSTALL_PREFIX = usr/local +#### END PROJECT SETTINGS #### + +# Generally should not need to edit below this line + +# Obtains the OS type, either 'Darwin' (OS X) or 'Linux' +UNAME_S:=$(shell uname -s) + +# Function used to check variables. Use on the command line: +# make print-VARNAME +# Useful for debugging and adding features +print-%: ; @echo $*=$($*) + +# Shell used in this makefile +# bash is used for 'echo -en' +SHELL = /bin/bash +# Clear built-in rules +.SUFFIXES: +# Programs for installation +INSTALL = install +INSTALL_PROGRAM = $(INSTALL) +INSTALL_DATA = $(INSTALL) -m 644 + +# Append pkg-config specific libraries if need be +ifneq ($(LIBS),) + COMPILE_FLAGS += $(shell pkg-config --cflags $(LIBS)) + LINK_FLAGS += $(shell pkg-config --libs $(LIBS)) +endif + +# Verbose option, to output compile and link commands +export V := false +export CMD_PREFIX := @ +ifeq ($(V),true) + CMD_PREFIX := +endif + +# Combine compiler and linker flags +release: export CFLAGS := $(CFLAGS) $(COMPILE_FLAGS) $(RCOMPILE_FLAGS) +release: export LDFLAGS := $(LDFLAGS) $(LINK_FLAGS) $(RLINK_FLAGS) +debug: export CFLAGS := $(CFLAGS) $(COMPILE_FLAGS) $(DCOMPILE_FLAGS) +debug: export LDFLAGS := $(LDFLAGS) $(LINK_FLAGS) $(DLINK_FLAGS) + +# Build and output paths +release: export BUILD_PATH := build/release +release: export BIN_PATH := bin/release +debug: export BUILD_PATH := build/debug +debug: export BIN_PATH := bin/debug +install: export BIN_PATH := bin/release + +# Find all source files in the source directory, sorted by most +# recently modified +ifeq ($(UNAME_S),Darwin) + SOURCES = $(shell find $(SRC_PATH) -name '*.$(SRC_EXT)' | sort -k 1nr | cut -f2-) +else + SOURCES = $(shell find $(SRC_PATH) -name '*.$(SRC_EXT)' -printf '%T@\t%p\n' \ + | sort -k 1nr | cut -f2-) +endif + +# fallback in case the above fails +rwildcard = $(foreach d, $(wildcard $1*), $(call rwildcard,$d/,$2) \ + $(filter $(subst *,%,$2), $d)) +ifeq ($(SOURCES),) + SOURCES := $(call rwildcard, $(SRC_PATH), *.$(SRC_EXT)) +endif + +# Set the object file names, with the source directory stripped +# from the path, and the build path prepended in its place +OBJECTS = $(SOURCES:$(SRC_PATH)/%.$(SRC_EXT)=$(BUILD_PATH)/%.o) +# Set the dependency files that will be used to add header dependencies +DEPS = $(OBJECTS:.o=.d) + +# Macros for timing compilation +ifeq ($(UNAME_S),Darwin) + CUR_TIME = awk 'BEGIN{srand(); print srand()}' + TIME_FILE = $(dir $@).$(notdir $@)_time + START_TIME = $(CUR_TIME) > $(TIME_FILE) + END_TIME = read st < $(TIME_FILE) ; \ + $(RM) $(TIME_FILE) ; \ + st=$$((`$(CUR_TIME)` - $$st)) ; \ + echo $$st +else + TIME_FILE = $(dir $@).$(notdir $@)_time + START_TIME = date '+%s' > $(TIME_FILE) + END_TIME = read st < $(TIME_FILE) ; \ + $(RM) $(TIME_FILE) ; \ + st=$$((`date '+%s'` - $$st - 86400)) ; \ + echo `date -u -d @$$st '+%H:%M:%S'` +endif + +# Version macros +# Comment/remove this section to remove versioning +USE_VERSION := false +# If this isn't a git repo or the repo has no tags, git describe will return non-zero +ifeq ($(shell git describe > /dev/null 2>&1 ; echo $$?), 0) + USE_VERSION := true + VERSION := $(shell git describe --tags --long --dirty --always | \ + sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)-\?.*-\([0-9]*\)-\(.*\)/\1 \2 \3 \4 \5/g') + VERSION_MAJOR := $(word 1, $(VERSION)) + VERSION_MINOR := $(word 2, $(VERSION)) + VERSION_PATCH := $(word 3, $(VERSION)) + VERSION_REVISION := $(word 4, $(VERSION)) + VERSION_HASH := $(word 5, $(VERSION)) + VERSION_STRING := \ + "$(VERSION_MAJOR).$(VERSION_MINOR).$(VERSION_PATCH).$(VERSION_REVISION)-$(VERSION_HASH)" + override CFLAGS := $(CFLAGS) \ + -D VERSION_MAJOR=$(VERSION_MAJOR) \ + -D VERSION_MINOR=$(VERSION_MINOR) \ + -D VERSION_PATCH=$(VERSION_PATCH) \ + -D VERSION_REVISION=$(VERSION_REVISION) \ + -D VERSION_HASH=\"$(VERSION_HASH)\" +endif + +# Standard, non-optimized release build +.PHONY: release +release: dirs +ifeq ($(USE_VERSION), true) + @echo "Beginning release build v$(VERSION_STRING)" +else + @echo "Beginning release build" +endif + @$(START_TIME) + @$(MAKE) all --no-print-directory + @echo -n "Total build time: " + @$(END_TIME) + +# Debug build for gdb debugging +.PHONY: debug +debug: dirs +ifeq ($(USE_VERSION), true) + @echo "Beginning debug build v$(VERSION_STRING)" +else + @echo "Beginning debug build" +endif + @$(START_TIME) + @$(MAKE) all --no-print-directory + @echo -n "Total build time: " + @$(END_TIME) + +# Create the directories used in the build +.PHONY: dirs +dirs: + @echo "Creating directories" + @mkdir -p $(dir $(OBJECTS)) + @mkdir -p $(BIN_PATH) + +# Installs to the set path +.PHONY: install +install: + @echo "Installing to $(DESTDIR)$(INSTALL_PREFIX)/bin" + @$(INSTALL_PROGRAM) $(BIN_PATH)/$(BIN_NAME) $(DESTDIR)$(INSTALL_PREFIX)/bin + +# Uninstalls the program +.PHONY: uninstall +uninstall: + @echo "Removing $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BIN_NAME)" + @$(RM) $(DESTDIR)$(INSTALL_PREFIX)/bin/$(BIN_NAME) + +# Removes all build files +.PHONY: clean +clean: + @echo "Deleting $(BIN_NAME) symlink" + @$(RM) $(BIN_NAME) + @echo "Deleting directories" + @$(RM) -r build + @$(RM) -r bin + +# Main rule, checks the executable and symlinks to the output +all: $(BIN_PATH)/$(BIN_NAME) + @echo "Making symlink: $(BIN_NAME) -> $<" + @$(RM) $(BIN_NAME) + @ln -s $(BIN_PATH)/$(BIN_NAME) $(BIN_NAME) + +# Link the executable +$(BIN_PATH)/$(BIN_NAME): $(OBJECTS) + @echo "Linking: $@" + @$(START_TIME) + $(CMD_PREFIX)$(CC) $(OBJECTS) $(LDFLAGS) -o $@ + @echo -en "\t Link time: " + @$(END_TIME) + +# Add dependency files, if they exist +-include $(DEPS) + +# Source file rules +# After the first compilation they will be joined with the rules from the +# dependency files to provide header dependencies +$(BUILD_PATH)/%.o: $(SRC_PATH)/%.$(SRC_EXT) + @echo "Compiling: $< -> $@" + @$(START_TIME) + $(CMD_PREFIX)$(CC) $(CFLAGS) $(INCLUDES) -MP -MMD -c $< -o $@ + @echo -en "\t Compile time: " + @$(END_TIME) From 8767abd8714c8d62c04be9412ccd7b1b9597cdcf Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Wed, 14 Jun 2017 14:10:45 +0800 Subject: [PATCH 58/67] udpate gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 440f742..50ca1e2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ channels/ *.iml *.apk *.pyc +*.d +*.o *.class .DS_Store a.out From 90654c5704b637134f7cc3ef8737e995232c43cc Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Thu, 15 Jun 2017 14:03:08 +0800 Subject: [PATCH 59/67] add sample channels --- .gitignore | 1 - build.gradle | 2 -- channels/channels.txt | 16 ++++++++++++++++ channels/free.txt | 7 +++++++ channels/paid.txt | 4 ++++ sample/build.gradle | 18 +++++++++--------- 6 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 channels/channels.txt create mode 100644 channels/free.txt create mode 100644 channels/paid.txt diff --git a/.gitignore b/.gitignore index 50ca1e2..827146f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ apks/ repo/ dist/ tmp/ -channels/ *.iml *.apk *.pyc diff --git a/build.gradle b/build.gradle index 96e57f6..76cc5ef 100644 --- a/build.gradle +++ b/build.gradle @@ -3,8 +3,6 @@ ext { buildToolsVersion = "25.0.3" minSdkVersion = 14 targetSdkVersion = 22 - versionName = "1.1.0-SNAPSHOT" - versionCode = 110 } group = GROUP diff --git a/channels/channels.txt b/channels/channels.txt new file mode 100644 index 0000000..468c80a --- /dev/null +++ b/channels/channels.txt @@ -0,0 +1,16 @@ +Google_Market#Google电子市场 +Hiapk_Market#安卓市场 +Yingyonghui_Market#应用汇市场 +ali_market#阿里云商店 +Xiaomi_Market#小米市场 +Yingyongbao_Market#腾讯应用宝市场 +Samsung_Market#三星市场 +OPPO_Market#OPPO市场 +Huawei_Market#华为市场 +amazon_market#亚马逊市场 +Meizu_Market#魅族市场 +3G_market#3G安卓市场 +WanDouJia_Parter#豌豆荚 +Baidu_Market#百度应用中心 +360_Market#360手机助手 +Taobao_Market#淘宝应用市场 diff --git a/channels/free.txt b/channels/free.txt new file mode 100644 index 0000000..5bbdea3 --- /dev/null +++ b/channels/free.txt @@ -0,0 +1,7 @@ +Cat1#hello2 +cat2#哈哈哈 +BigCat#hello1 +田园猫 +橘Cat#gogogo +GoodCat +Special@Cat%001 # oooo diff --git a/channels/paid.txt b/channels/paid.txt new file mode 100644 index 0000000..e76957d --- /dev/null +++ b/channels/paid.txt @@ -0,0 +1,4 @@ +Dog1 +Dog2#d1 +Dog3#d5 +金毛# it is a dog \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index f91e1f2..6628e8e 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.9.2-SNAPSHOT' + ext.packer_version = '1.9.2' repositories { maven { url '/tmp/repo/' } @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath "com.android.tools.build:gradle:2.2.2" + classpath "com.android.tools.build:gradle:2.2.3" classpath "com.mcxiaoke.packer-ng:plugin:$packer_version" } } @@ -33,11 +33,11 @@ packer { archiveOutput = new File(project.rootProject.buildDir, "apks") // channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', // 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] -// channelFile = new File(project.rootDir, "markets.txt") +// channelFile = new File(project.rootDir, "channels.txt") channelMap = [ - "Cat" : project.rootProject.file("channels/cat.txt"), - "Dog" : project.rootProject.file("channels/dog.txt"), - "Fish": project.rootProject.file("channels/channels.txt") + "free" : project.rootProject.file("channels/free.txt"), + "paid" : project.rootProject.file("channels/paid.txt"), + "other": project.rootProject.file("channels/channels.txt") ] } //packer-end @@ -97,11 +97,11 @@ android { } productFlavors { - Dog {} + free {} - Cat {} + paid {} - Fish {} + other {} } lintOptions { From 355f89bf553e2d8f24ca48c0fc3b844fc5b39e15 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Thu, 15 Jun 2017 15:09:49 +0800 Subject: [PATCH 60/67] autopep8 fix python script --- tools/packer-ng-v2.py | 86 +++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index 79f3aaf..f98bda3 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -90,31 +90,40 @@ def __init__(self, buf, littleEndian=True): self.sign = '<' if littleEndian else '>' def getShort(self, offset=0): - return struct.unpack('{}h'.format(self.sign), self.buf[offset:offset + 2])[0] + return struct.unpack('{}h'.format(self.sign), + self.buf[offset:offset + 2])[0] def getUShort(self, offset=0): - return struct.unpack('{}H'.format(self.sign), self.buf[offset:offset + 2])[0] + return struct.unpack('{}H'.format(self.sign), + self.buf[offset:offset + 2])[0] def getInt(self, offset=0): - return struct.unpack('{}i'.format(self.sign), self.buf[offset:offset + 4])[0] + return struct.unpack('{}i'.format(self.sign), + self.buf[offset:offset + 4])[0] def getUInt(self, offset=0): - return struct.unpack('{}I'.format(self.sign), self.buf[offset:offset + 4])[0] + return struct.unpack('{}I'.format(self.sign), + self.buf[offset:offset + 4])[0] def getLong(self, offset=0): - return struct.unpack('{}q'.format(self.sign), self.buf[offset:offset + 8])[0] + return struct.unpack('{}q'.format(self.sign), + self.buf[offset:offset + 8])[0] def getULong(self, offset=0): - return struct.unpack('{}Q'.format(self.sign), self.buf[offset:offset + 8])[0] + return struct.unpack('{}Q'.format(self.sign), + self.buf[offset:offset + 8])[0] def getFloat(self, offset=0): - return struct.unpack('{}f'.format(self.sign), self.buf[offset:offset + 4])[0] + return struct.unpack('{}f'.format(self.sign), + self.buf[offset:offset + 4])[0] def getDouble(self, offset=0): - return struct.unpack('{}d'.format(self.sign), self.buf[offset:offset + 8])[0] + return struct.unpack('{}d'.format(self.sign), + self.buf[offset:offset + 8])[0] def getChars(self, offset=0, size=16): - return struct.unpack('{}{}'.format(self.sign, 's' * size), self.buf[offset:offset + size]) + return struct.unpack('{}{}'.format(self.sign, 's' * size), + self.buf[offset:offset + size]) ##################################################################### @@ -166,11 +175,12 @@ def createMap(apk): with open(apk, "rb") as f: size = os.path.getsize(apk) offset = max(0, size - BlOCK_MAX_SIZE) + length = min(size, BlOCK_MAX_SIZE) offset = offset - offset % mmap.PAGESIZE logger.debug('file size=%s', size) logger.debug('file offset=%s', offset) return mmap.mmap(f.fileno(), - length=BlOCK_MAX_SIZE, + length=length, offset=offset, access=mmap.ACCESS_READ) @@ -246,13 +256,15 @@ def findByZipSections(apk): logger.debug('eocdStartOffset:%s', eocdStartOffset) if centralDirEndOffset != eocdStartOffset: raise SignatureNotFoundException( - "ZIP Central Directory is not immediately followed by End of Central Directory" - + ". CD end: " + centralDirEndOffset - + ", EoCD start: " + eocdStartOffset) + "ZIP Central Directory is not " + "immediately followed by " + "End of Central Directory. CD end: {} eocd start: {}" + .format(centralDirEndOffset, eocdStartOffset)) if centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE: raise SignatureNotFoundException( - "APK too small for APK Signing Block. ZIP Central Directory offset: " - + centralDirStartOffset) + "APK too small for APK Signing Block. " + "ZIP Central Directory offset:{} " + .format(centralDirStartOffset)) fStart = centralDirStartOffset - 24 mStart = centralDirStartOffset - 16 @@ -282,9 +294,10 @@ def findByZipSections(apk): logger.debug('apkSigBlockSizeInFooter:%s', apkSigBlockSizeInFooter) if apkSigBlockSizeInFooter < footerSize or \ - apkSigBlockSizeInFooter > sys.maxint - 8: + apkSigBlockSizeInFooter > sys.maxsize - 8: raise SignatureNotFoundException( - "APK Signing Block size out of range: " + apkSigBlockSizeInFooter) + "APK Signing Block size out of range: {}" + .format(apkSigBlockSizeInFooter)) totalSize = apkSigBlockSizeInFooter + 8 logger.debug('totalSize:%s', totalSize) @@ -302,8 +315,9 @@ def findByZipSections(apk): if apkSigBlockSizeInHeader != apkSigBlockSizeInFooter: raise SignatureNotFoundException( - "APK Signing Block sizes in header and footer do not match: " - + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter) + "APK Signing Block sizes in header and" + "footer do not match: {} vs {}" + .format(apkSigBlockSizeInHeader, apkSigBlockSizeInFooter)) block = mm[apkSigBlockOffset:apkSigBlockOffset + totalSize] mm.close() @@ -344,20 +358,22 @@ def parseApkSigningBlock(block, blockId): logger.debug('entryCount:%s', entryCount) if size - position < 8: raise SignatureNotFoundException( - 'Insufficient data to read size of APK Signing Block entry: {}'.format(entryCount)) + "Insufficient data to read size " + "of APK Signing Block entry: {}" + .format(entryCount)) lenLong = bd.getLong(position) logger.debug('lenLong:%s', lenLong) position += 8 - if lenLong < 4 or lenLong > sys.maxint - 8: + if lenLong < 4 or lenLong > sys.maxsize - 8: raise SignatureNotFoundException( - "APK Signing Block entry #" + entryCount - + " size out of range: " + lenLong) + "APK Signing Block entry #{} size out of range: {}" + .format(entryCount, lenLong)) nextEntryPos = position + lenLong logger.debug('nextEntryPos:%s', nextEntryPos) if nextEntryPos > size: SignatureNotFoundException( - "APK Signing Block entry #" + entryCount + " size out of range: " + len - + ", available: " + (size - position)) + "APK Signing Block entry #{}, available: {}" + .format(entryCount, (size - position))) sid = bd.getInt(position) logger.debug('blockId:%s', hex(sid)) position += 4 @@ -392,8 +408,9 @@ def findZipSections(mm): logger.debug('cdStartOffset:%s', cdStartOffset) if cdStartOffset > eocdOffset: raise ZipFormatException( - "ZIP Central Directory start offset out of range: " + cdStartOffset - + ". ZIP End of Central Directory offset: " + eocdOffset) + "ZIP Central Directory start offset out of range: {}" + ". ZIP End of Central Directory offset: {}" + .format(cdStartOffset, eocdOffset)) cdSizeBytes = ed.getUInt(ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET) logger.debug('cdSizeBytes:%s', cdSizeBytes) cdEndOffset = cdStartOffset + cdSizeBytes @@ -401,8 +418,8 @@ def findZipSections(mm): if cdEndOffset > eocdOffset: raise ZipFormatException( "ZIP Central Directory overlaps with End of Central Directory" - + ". CD end: " + cdEndOffset - + ", EoCD start: " + eocdOffset) + ". CD end: {}, EoCD start: {}" + .format(cdEndOffset, eocdOffset)) cdRecordCount = ed.getUShort( ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET) logger.debug('cdRecordCount:%s', cdRecordCount) @@ -441,16 +458,15 @@ def findEocdStartOffset(buf): maxCommentLength = min( archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE) logger.debug('maxCommentLength:%s', maxCommentLength) - eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE - logger.debug('eocdWithEmptyCommentStartPosition:%s', - eocdWithEmptyCommentStartPosition) + eocdEmptyCommentStartPos = archiveSize - ZIP_EOCD_REC_MIN_SIZE + logger.debug('eocdEmptyCommentStartPos:%s', + eocdEmptyCommentStartPos) expectedCommentLength = 0 eocdOffsetInBuf = -1 while expectedCommentLength <= maxCommentLength: - eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength + eocdStartPos = eocdEmptyCommentStartPos - expectedCommentLength logger.debug('expectedCommentLength:%s', expectedCommentLength) # logger.debug('eocdStartPos:%s', eocdStartPos) - # logger.debug('eocdStart:%s', to_hex(buf[eocdStartPos:eocdStartPos+4])) seg = ByteDecoder(buf).getInt(eocdStartPos) logger.debug('seg:%s', hex(seg)) if seg == ZIP_EOCD_REC_SIG: @@ -490,7 +506,7 @@ def getChannel(apk): try: zp = zipfile.ZipFile(apk) zp.testzip() - content = findBlockByPluginMagic(apk) + content = findBlockByZipSections(apk) values = parseValues(content) if values: channel = values.get(PLUGIN_CHANNEL_KEY) From 4cde1c686f1946b8f85cdecc0ba75a14bb300c19 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Thu, 15 Jun 2017 15:10:04 +0800 Subject: [PATCH 61/67] rename sample to app --- {sample => app}/.gitignore | 0 {sample => app}/android.keystore | Bin {sample => app}/build.gradle | 0 {sample => app}/custom.txt | 0 {sample => app}/gradle.properties | 0 {sample => app}/local.properties | 0 {sample => app}/src/main/AndroidManifest.xml | 0 .../gen/com/mcxiaoke/mpp/sample/BuildConfig.java | 0 .../main/gen/com/mcxiaoke/mpp/sample/Manifest.java | 0 .../src/main/gen/com/mcxiaoke/mpp/sample/R.java | 0 .../com/mcxiaoke/packer/samples/MainActivity.java | 0 .../src/main/res/drawable-xxhdpi/ic_launcher.png | Bin huge_markets_test.py | 11 ++++++----- settings.gradle | 2 +- test-build.sh | 2 +- test-market.sh | 2 +- 16 files changed, 9 insertions(+), 8 deletions(-) rename {sample => app}/.gitignore (100%) rename {sample => app}/android.keystore (100%) rename {sample => app}/build.gradle (100%) rename {sample => app}/custom.txt (100%) rename {sample => app}/gradle.properties (100%) rename {sample => app}/local.properties (100%) rename {sample => app}/src/main/AndroidManifest.xml (100%) rename {sample => app}/src/main/gen/com/mcxiaoke/mpp/sample/BuildConfig.java (100%) rename {sample => app}/src/main/gen/com/mcxiaoke/mpp/sample/Manifest.java (100%) rename {sample => app}/src/main/gen/com/mcxiaoke/mpp/sample/R.java (100%) rename {sample => app}/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java (100%) rename {sample => app}/src/main/res/drawable-xxhdpi/ic_launcher.png (100%) diff --git a/sample/.gitignore b/app/.gitignore similarity index 100% rename from sample/.gitignore rename to app/.gitignore diff --git a/sample/android.keystore b/app/android.keystore similarity index 100% rename from sample/android.keystore rename to app/android.keystore diff --git a/sample/build.gradle b/app/build.gradle similarity index 100% rename from sample/build.gradle rename to app/build.gradle diff --git a/sample/custom.txt b/app/custom.txt similarity index 100% rename from sample/custom.txt rename to app/custom.txt diff --git a/sample/gradle.properties b/app/gradle.properties similarity index 100% rename from sample/gradle.properties rename to app/gradle.properties diff --git a/sample/local.properties b/app/local.properties similarity index 100% rename from sample/local.properties rename to app/local.properties diff --git a/sample/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml similarity index 100% rename from sample/src/main/AndroidManifest.xml rename to app/src/main/AndroidManifest.xml diff --git a/sample/src/main/gen/com/mcxiaoke/mpp/sample/BuildConfig.java b/app/src/main/gen/com/mcxiaoke/mpp/sample/BuildConfig.java similarity index 100% rename from sample/src/main/gen/com/mcxiaoke/mpp/sample/BuildConfig.java rename to app/src/main/gen/com/mcxiaoke/mpp/sample/BuildConfig.java diff --git a/sample/src/main/gen/com/mcxiaoke/mpp/sample/Manifest.java b/app/src/main/gen/com/mcxiaoke/mpp/sample/Manifest.java similarity index 100% rename from sample/src/main/gen/com/mcxiaoke/mpp/sample/Manifest.java rename to app/src/main/gen/com/mcxiaoke/mpp/sample/Manifest.java diff --git a/sample/src/main/gen/com/mcxiaoke/mpp/sample/R.java b/app/src/main/gen/com/mcxiaoke/mpp/sample/R.java similarity index 100% rename from sample/src/main/gen/com/mcxiaoke/mpp/sample/R.java rename to app/src/main/gen/com/mcxiaoke/mpp/sample/R.java diff --git a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java b/app/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java similarity index 100% rename from sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java rename to app/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java diff --git a/sample/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png similarity index 100% rename from sample/src/main/res/drawable-xxhdpi/ic_launcher.png rename to app/src/main/res/drawable-xxhdpi/ic_launcher.png diff --git a/huge_markets_test.py b/huge_markets_test.py index 071718e..7453547 100755 --- a/huge_markets_test.py +++ b/huge_markets_test.py @@ -4,11 +4,12 @@ # @Date: 2015-11-25 14:30:02 import subprocess import os +import sys -with open('huge_markets.txt','w+') as f: - for i in range(1000): - f.write("Test Market %s#test market %s\n" % (i,i)) - f.write("中文:MARKET%s#test market %s\n" % (i,i)) +with open('huge_markets.txt', 'w') as f: + for i in range(int(sys.argv[1])): + f.write("Test Market %s#test market %s\n" % (i, i)) + f.write("中文:MARKET%s#test market %s\n" % (i, i)) -subprocess.call(["./gradlew", "-Pmarket=huge_markets.txt", "clean", "archiveApkRelease"]) +subprocess.check_output(["./gradlew", "-Pchannels=@huge_markets.txt", "-Poutput=tmp", "clean", "apkPaidRelease"]) os.remove('huge_markets.txt') diff --git a/settings.gradle b/settings.gradle index 0ab5d7e..d515779 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,4 +2,4 @@ include ':common' include ':plugin' include ':cli' include ':helper' -include ':sample' +include ':app' diff --git a/test-build.sh b/test-build.sh index 09d93bb..e8829fe 100755 --- a/test-build.sh +++ b/test-build.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash ./deploy-local.sh echo "test clean build" -./gradlew clean build --stacktrace $1 $2 +./gradlew clean assemblePaidRelease --stacktrace $1 $2 diff --git a/test-market.sh b/test-market.sh index 7dd4aeb..c0bd378 100755 --- a/test-market.sh +++ b/test-market.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash ./deploy-local.sh echo "------ build for markets running..." -./gradlew -Pmarket=markets.txt clean apkRelease $1 $2 +./gradlew -Pchannels=@channels/channels.txt clean apkRelease $1 $2 echo "------ build for markets finished!" From cfd16e18c5a58221d8806d73eac275aa55eed6e3 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Thu, 15 Jun 2017 15:15:05 +0800 Subject: [PATCH 62/67] update readme --- readme.md | 82 +++++++++++++++++++++++++------------------------------ 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/readme.md b/readme.md index 214e612..c017e07 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -PackerNg V2 (即将发布) +PackerNg V2 ======== 极速渠道打包工具 @@ -23,9 +23,9 @@ V2版只支持`APK Signature Scheme v2`,要求在 `signingConfigs` 里 `v2Sign ```groovy buildscript { - dependencies{ - classpath 'com.mcxiaoke.packer-ng:plugin:2.0.0' - } + dependencies{ + classpath 'com.mcxiaoke.packer-ng:plugin:2.0.0' + } } ``` @@ -35,7 +35,7 @@ buildscript { apply plugin: 'packer' dependencies { - compile 'com.mcxiaoke.packer-ng:helper:2.0.0' + compile 'com.mcxiaoke.packer-ng:helper:2.0.0' } ``` @@ -70,68 +70,60 @@ packer { 渠道名列表文件是纯文本文件,按行读取,每行一个渠道,行首和行尾的空白会被忽略,如果有注释,渠道名和注释之间用 `#` 分割。 -渠道名建议尽量使用规范的**中英文和数字**,不要使用特殊字符和不可见字符。示例: - -``` -Google_Play#play store market -Gradle_Test# 这是注释 -SomeMarket#some market -中文渠道 # comments -HelloWorld -``` +渠道名建议尽量使用规范的**中英文和数字**,不要使用特殊字符和不可见字符。示例:[channels.txt](blob/v2dev/channels/channels.txt) ### 集成打包 -* 需要打包 `release` 类型时,最简单的命令如下: +* 项目中没有使用 `productFlavors` - ```shell - ./gradlew clean apkRelease - ``` + ```shell + ./gradlew clean apkRelease + ``` * 项目中使用了 `productFlavors` - 如果项目中指定了多个 `flavor` ,需要指定需要打渠道包的 `flavor` 名字,假设你有 `Paid` `Free` 两个 `flavor` ,打包的时候命令如下: + 如果项目中指定了多个 `flavor` ,需要指定需要打渠道包的 `flavor` 名字,假设你有 `Paid` `Free` 两个 `flavor` ,打包的时候命令如下: - ```shell - ./gradlew clean apkPaidRelease - ./gradlew clean apkFreeRelease - ``` - - 直接使用 `./gradlew clean apkRelease` 会输出所有 `flavor` 的渠道包。 + ```shell + ./gradlew clean apkPaidRelease + ./gradlew clean apkFreeRelease + ``` + + 直接使用 `./gradlew clean apkRelease` 会输出所有 `flavor` 的渠道包。 * 通过参数直接指定渠道列表(会覆盖`build.gradle`中的属性): - ```shell - ./gradlew clean apkRelease -Pchannels=ch1,ch2,douban,google - ``` - - 渠道数目很少时可以使用此种方式。 + ```shell + ./gradlew clean apkRelease -Pchannels=ch1,ch2,douban,google + ``` + + 渠道数目很少时可以使用此种方式。 * 通过参数指定渠道列表文件的位置(会覆盖`build.gradle`中的属性): - ```shell - ./gradlew clean apkRelease -Pchannels=@channels.txt - ``` - - 使用@符号指定渠道列表文件的位置,使用相对于项目根目录的相对路径。 + ```shell + ./gradlew clean apkRelease -Pchannels=@channels.txt + ``` + + 使用@符号指定渠道列表文件的位置,使用相对于项目根目录的相对路径。 * 还可以指定输出目录和文件名格式模版: - ```shell - ./gradlew clean apkRelease -Poutput=build/apks - ./gradlew clean apkRelease -Pformat=${versionName}-${channel} - ``` - - 这些参数 `channels` `output` `format` 可以组合使用,命令行参数会覆盖 `build.gradle` 对应的属性。 + ```shell + ./gradlew clean apkRelease -Poutput=build/apks + ./gradlew clean apkRelease -Pformat=${versionName}-${channel} + ``` + + 这些参数 `channels` `output` `format` 可以组合使用,命令行参数会覆盖 `build.gradle` 对应的属性。 * Gradle打包命令说明 - 渠道打包的Task名字是 `apk${flavor}${buildType}` buildType一般是release,也可以是你自己指定的beta或者someOtherType,如果没有 `flavor` 可以忽略,使用时首字母需要大写,假设 `flavor` 是 `Paid`,`release`类型对应的任务名是 `apkPaidRelease`,`beta`类型对应的任务名是 `apkPaidBetaBeta`,其它的以此类推。 - + 渠道打包的Task名字是 `apk${flavor}${buildType}` buildType一般是release,也可以是你自己指定的beta或者someOtherType,如果没有 `flavor` 可以忽略,使用时首字母需要大写,假设 `flavor` 是 `Paid`,`release`类型对应的任务名是 `apkPaidRelease`,`beta`类型对应的任务名是 `apkPaidBetaBeta`,其它的以此类推。 + * 特别提示 - 如果你同时使用其它的资源压缩工具或应用加固功能,请使用命令行脚本打包增加渠道信息,增加渠道信息需要放在APK处理过程的最后一步。 - + 如果你同时使用其它的资源压缩工具或应用加固功能,请使用命令行脚本打包增加渠道信息,增加渠道信息需要放在APK处理过程的最后一步。 + ### 脚本打包 除了使用Gradle集成以外,还可以使用项目提供的Java脚本打包,Jar位于本项目的 `tools` 目录,请使用最新版,以下用 `packer-ng` 指代 `java -jar tools/packer-ng-2.0.0.jar`,示例: From 126520f603f37d520e9330017b83bb70005a9943 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Thu, 15 Jun 2017 15:25:57 +0800 Subject: [PATCH 63/67] update readme --- readme.md | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/readme.md b/readme.md index c017e07..c572f2a 100644 --- a/readme.md +++ b/readme.md @@ -2,17 +2,15 @@ PackerNg V2 ======== 极速渠道打包工具 +- **v2.0.0 - 2017.06.30** - 全新发布,支持V2签名模式,包含多项优化 + ## 特别提示 V2版只支持`APK Signature Scheme v2`,要求在 `signingConfigs` 里 `v2SigningEnabled true` 启用新版签名模式,并使用`2.2.0`以上版本的Gradle插件,如果你需要使用旧版本,看这里 [v1.0.9](https://github.com/mcxiaoke/packer-ng-plugin/tree/v1.0.9)。 -## 最新版本 - -- **v2.0.0 - 2017.06.30** - 全新发布,支持V2签名模式,包含多项优化 - ## 项目介绍 -[**packer-ng-plugin**](https://github.com/mcxiaoke/packer-ng-plugin) 是下一代Android渠道打包工具Gradle插件,支持极速打包,**100**个渠道包只需要**10**秒钟,速度是 [**gradle-packer-plugin**](https://github.com/mcxiaoke/gradle-packer-plugin) 的**300**倍以上,可方便的用于CI系统集成。 +[**packer-ng-plugin**](https://github.com/mcxiaoke/packer-ng-plugin) 是下一代Android渠道打包工具Gradle插件,支持极速打包,**100**个渠道包只需要**10**秒钟,速度是 [**gradle-packer-plugin**](https://github.com/mcxiaoke/gradle-packer-plugin) 的**300**倍以上,可方便的用于CI系统集成,同时提供命令行打包脚本,渠道读取提供Python和C语言的实现。 ## 使用指南 @@ -140,32 +138,39 @@ packer-ng generate --channels=ch1,ch2,ch3 --output=build/archives app.apk packer-ng generate --channels=@channels.txt --output=build/archives app.apk ``` -* 验证渠道包的渠道信息: +* 验证渠道信息: ```shell packer-ng verify app.apk ``` -* 其它参数可以运行命令查看帮助 +* 运行命令查看帮助 ```shell java -jar tools/packer-ng-2.0.0.jar --help ``` -* 还可以使用Python脚本读取渠道: +* Python脚本读取渠道: ```shell python tools/packer-ng-v2.py app.apk ``` -### 代码中读取渠道 +* C程序读取渠道: -`PackerNg.getMarket(Context)`内部缓存了结果,不会重复解析 +```shell +cd tools +make +make install +packer app.apk +``` + +### 代码中读取渠道 ```java -// 如果没有找到渠道信息,默认返回的是"" +// 如果没有找到渠道信息或遇到错误,默认返回的是"" // com.mcxiaoke.packer.helper.PackerNg -String market = PackerNg.getMarket(Context) +String channel = PackerNg.getChannel(Context) ``` ### 文件名格式模版 @@ -187,6 +192,13 @@ String market = PackerNg.getMarket(Context) * *buildTime* - `buildTime` (编译构建日期时间) * *fileSHA1* - `fileSHA1 ` (最终APK文件的SHA1哈希值) +------ + +## 其它说明 + +渠道读取C语言实现使用 [GenericMakefile](https://github.com/mbcrawfo/GenericMakefile) 构建,[APK Signing Block](https://source.android.com/security/apksigning/v2) 读取和写入Java实现修改自 [apksig](https://android.googlesource.com/platform/tools/apksig/+/master) 和 [walle](https://github.com/Meituan-Dianping/walle/tree/master/payload_writer) ,特此致谢。 + + ------ ## 关于作者 From 8896a68b3c5612f52f5682c6e1173ff425e49401 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Thu, 15 Jun 2017 16:56:02 +0800 Subject: [PATCH 64/67] fix and tweak c read program --- tools/read.c | 88 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/tools/read.c b/tools/read.c index 1573a50..e01a5cf 100644 --- a/tools/read.c +++ b/tools/read.c @@ -10,6 +10,7 @@ #include #include #include +#include #include /* @@ -17,21 +18,23 @@ * https://en.wikipedia.org/wiki/Mmap */ +static const char *apk_ext = ".apk"; static const off_t block_size = 0x100000; static const char *sep_kv = "∘"; static const char *sep_line = "∙"; static const char *magic = "Packer Ng Sig V2"; // static const char *key = "CHANNEL"; +// static const char *version = "v2.0.0"; #define handle_error(msg) \ do { \ - perror(msg); \ + printf(msg); \ exit(EXIT_FAILURE); \ } while (0) #define handle_not_found() \ do { \ - printf("Channel not found.\n"); \ + printf("Channel not found\n"); \ exit(EXIT_FAILURE); \ } while (0) @@ -86,6 +89,24 @@ int32_t kmp_search(const char *str, int slen, const char *word, int wlen) { return -1; } +int str_has_suffix(const char *str, const char *suf) { + const char *a = str + strlen(str); + const char *b = suf + strlen(suf); + while (a != str && b != suf) { + if (*--a != *--b) + break; + } + return b == suf && *a == *b; +} + +// ensure write '\0' at end +// http://en.cppreference.com/w/c/string/byte/strncpy +char *strncpy_2(char *dest, const char *src, size_t count) { + char *ret = strncpy(dest, src, count); + dest[count] = '\0'; + return ret; +} + int main(int argc, char *argv[]) { char *addr; int fd; @@ -94,16 +115,25 @@ int main(int argc, char *argv[]) { size_t length; if (argc < 2) { - fprintf(stderr, "Usage: %s app.apk - show channel of provided apk\n", - argv[0]); + printf("Usage: %s app.apk (show apk channel)\n", argv[0]); exit(EXIT_FAILURE); } - fd = open(argv[1], O_RDONLY); - if (fd == -1) - handle_error("open"); - - if (fstat(fd, &sb) == -1) /* To obtain file siclearze */ - handle_error("fstat"); + char *fn = argv[1]; + // printf("file name: %s\n", fn); + if (!str_has_suffix(fn, apk_ext)) { + handle_error("Not apk file\n"); + } + fd = open(fn, O_RDONLY); + if (fd == -1) { + handle_error("No such file\n"); + } + if (fstat(fd, &sb) == -1) { + handle_error("Can not read"); + } + // printf("file mode=%d\n", sb.st_mode); + if (!S_ISREG(sb.st_mode)) { + handle_error("Not regular file\n"); + } if (sb.st_size < block_size) { offset = 0; } else { @@ -117,8 +147,9 @@ int main(int argc, char *argv[]) { // printf("mmap real size=%zu\n", pa_length); // printf("mmap real offset=%lld\n", pa_offset); addr = mmap(NULL, pa_length, PROT_READ, MAP_PRIVATE, fd, pa_offset); - if (addr == MAP_FAILED) - handle_error("mmap"); + if (addr == MAP_FAILED) { + handle_error("Can not mmap\n"); + } int32_t index = kmp_search(addr, pa_length, magic, strlen(magic)); if (index == -1) { @@ -133,25 +164,28 @@ int main(int argc, char *argv[]) { if (payload_len < 0 || payload_len > block_size) { handle_not_found(); } - char *payload = malloc(payload_len + 1); - strncpy(payload, &addr[li + 4], payload_len); + // char *payload = malloc(payload_len + 1); + char payload[payload_len + 1]; + strncpy_2(payload, &addr[li + 4], payload_len); + // payload[payload_len] = '\0'; // printf("payload=%s\n", payload); - char *pos1 = strstr(payload, sep_kv); - char *pos2 = strstr(payload, sep_line); - if (pos1 == NULL || pos2 == NULL) { + char *pos_start = strstr(payload, sep_kv); + char *pos_end = strstr(payload, sep_line); + if (pos_start == NULL || pos_end == NULL) { handle_not_found(); } - size_t n1 = pos1 - payload + strlen(sep_kv); - size_t n2 = pos2 - payload; - size_t clen = n2 - n1; - // printf("n1=%zu, n2=%zu, clen=%zu\n", n1, n2, clen); - char *channel = malloc(clen); - strncpy(channel, &payload[n1], clen); - printf("Channel: %s\n", channel); - free(payload); - free(channel); + size_t c_start = pos_start - payload + strlen(sep_kv); + size_t c_end = pos_end - payload; + size_t c_len = c_end - c_start; + // printf("c_start=%zu, c_end=%zu, clen=%zu\n", c_start, c_end, clen); + // char *channel = malloc(clen + 1); + char channel[c_len + 1]; + strncpy_2(channel, &payload[c_start], c_len); + // channel[c_len] = '\0'; + printf("%s\n", channel); + // free(payload); + // free(channel); munmap(addr, pa_length); close(fd); - exit(EXIT_SUCCESS); } \ No newline at end of file From ab345d5754fee0563dee22dd8dc45ac33db160e1 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Thu, 15 Jun 2017 17:02:15 +0800 Subject: [PATCH 65/67] release v1.9.3 for test --- app/build.gradle | 2 +- gradle.properties | 2 +- ...acker-ng-1.9.2.jar => packer-ng-1.9.3.jar} | Bin 224452 -> 224453 bytes 3 files changed, 2 insertions(+), 2 deletions(-) rename tools/{packer-ng-1.9.2.jar => packer-ng-1.9.3.jar} (94%) diff --git a/app/build.gradle b/app/build.gradle index 6628e8e..6c120c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.9.2' + ext.packer_version = '1.9.3-SNAPSHOT' repositories { maven { url '/tmp/repo/' } diff --git a/gradle.properties b/gradle.properties index cfa5674..19f3ecb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=1.9.2 +VERSION_NAME=1.9.3 VERSION_CODE=110 GROUP=com.mcxiaoke.packer-ng diff --git a/tools/packer-ng-1.9.2.jar b/tools/packer-ng-1.9.3.jar similarity index 94% rename from tools/packer-ng-1.9.2.jar rename to tools/packer-ng-1.9.3.jar index 6d4c2d68f5b2d06f4b73164637cea66a075aa322..7454aa4644d4a5162c60aa80b55150515fd29165 100644 GIT binary patch delta 3577 zcmZve2~bp57C`@h7m=kKntf?RKolVg&EAL&4JfOuq9}`?hzg=&rb@>#C}%g7LyETWEs8DrE^EABdpQKKQQDXod4bKn2abSKkQ-T&Tmmv`TN@BM!N zxTHRQNv+Py0xLT~u(1(}-a6f;9%D<7%-1{)|9Y@(qW|hF`=dI|n-3aaH2)Ve>fir5 zBhS$;!Qnf@pL@#Q+v$I4d*Y3*u?ZEq$6~u97i#amx%q-Pb!UvH=eGFCz=mm%`AjlABLUexg6{=Oi&iel8Wm<>wAj&!62l#qIW7 zz?-?Eo3uq8C|i%H6J;wngbkC0XqR!|KyI1muANHRr51!ft+XkAY&W_eRm%>gftx`V zo(FwkB@6G4OHzcZx*Aa?84+=|vd|Qt#$@5NK~&OO?MuFIV1lp%cv?K92lZN)q-K{S zalcCvJ8}Y2sKjq=A({*O9qmA6B}={zz?dwJ(ty9cETw7T?=H7h~5 zV=D-;CQm-zGc`6uLl7FHVYp;;u*ZIikq3PdMTG~un9P17EjNN7gjr;ofvck-K@RPW zrqH}VUW2qT&<^cm26B4Gkm08w+29yH9OuMmiQSr%0a^x?yr0gs{C__Qefx5W&1sW%+s>=-K6LE=#rfYRNCh0T)jK z4bGkb-YhbIz~F*=CQyR*Lec~AC6|}2l-gG2H#*9wu#Vv&Y&*8uX10InBXDNMx$N z$+Ff68EpASvfNCvxCqZ0VFrutA>=33^;lze0q;Z-C4^d$$0DvN3bnusm#2U?swUAG-8W?{ z)!kBI#Uxsn;9G;hnfG6Vce&frn)s*SeRPYulC>5%ilbe}Kbs9`y+x*PPkA9*jR%B7ot^iSkcIjZpwgxB; zx^zfpEQ%{WxG5bH#RGUI9SYgD7|sIldW@*UkZcM*87G=IP}xz&wo^qfKA{j+INj2g$iA7Wki9uD z5+7#MPO&OABSksXtfyrrQoKrIH0Aw`qK4kD%BP{R3zz27EDx+v{2t~) zDGP5hDgL;z2%Io>CY_dDd9*iNTg+^Kok@#wzty6L*IUgT!ZwO3W!UC?3U2DQv{~bg zdp9uz<-W4FwFN~h9vx85@mPc%LjhvSH1`nfWd z;&brs6;rm`p@Y;5 z^GqwvlwI&Jgo6(zSEjA9gSDu*f-xq-7F^sN9RYkJ3|SmWCj)EnD& zPDWu`I~$4Pi)f64?=$lg^fBsYQyT4d#qZE;8Z>@~qWI0Bw`F~&qI}lLls|lB@`byZ z`Rgiex8jvsNw1b?b0CuG_L)c~lRm?L&!yMh7nRhYbF!jG|G6}$A3x_7J-$5`3dHkx zb1uDbn@%a-i>hb^E}u~p-8?G(N)_$D8)r?VfUUi#D7_b%8`f8m&&hez#QP60e?Eku z?jdu5AfqhpHhIoNeSF zd{qr&`K1;&%?Gbx{YfWnL24A;u&f3&GOw!vEr6XuqwGL4-uymGk(sK`U%h*Tu2e7S z5b@TB7mxhu}J+mNbx@$4E7j0mM$&yrzGF*(WjOs i*BY$Yb8G2}%v-U9(_)=M*X!um?5PEf^{5D-y#E8Aa8zyp delta 3479 zcmZWr2~bp57XAO-76H4VS%qd%5J7QknzaQQ8W#|hRRu&4MMaUg)o6@@BZ`@5)M&cG z#WIw*O#*6Z+;CwOba2#>h#F_yMJrBbV!4eoW~`}LOY-jBiqKs4tgP6-`#-QscXoSS{=LNHs|ViJ8kV)T!-tmJ<2>P zXXMnLc#(P{cF*#{VQoh?`xmIoKRVp#_f0a~x^S#vS9P05;kCD&E>oj^_BO3f|IN@g z-=k^n?{+=cjlH73pX|H3ZT`^4@9H|fsruMBH^)74$;|;To^Q;(_2k29_XTO@_U86G zEo*-KsWGInE~K(_@75=F-9=?CYqtc*0l?RBa;R9B%3)&ty&S{WqmSj?{rCnSXUHCw z9hxA)p4TJ@R^>FRkJv=H4F@MuOTGD^eawE1B>l<9vMqMV(OS>*8lg}av>n9eT}Yiu zY_<ot1#HJ%&R@|zNBt45~lC%%FTRam3nsx5x-?^LRCU-My zy~MTgwgW`+O_SCU)WUbu04HF=Ra1}_0z@TEDL!yaE%e@pz;+S3X~$5wY$vc%JzSE3 z4jK&xi0D3XTx0;F2(#Y+KKmtZNzpb< z9`CINt0Ex;??-^Yi1l*>H!1FbOZMk>n*Kh-YEh z8sv!TC~G@>qKJ4FA_#3*Uo43NFJZBo6Q2&Z5>D72W$l&rKyWV5vAm%F#9r~P>8;wK zm>zBQUl>hp;tW-;EQGhg+ z(Q+ofi2)<47-Pvg?tx>B5XugX!B!&-z%51ylfAIT2)XReB!O8LW~nWGA3Ncqv7p6S zqd~_ESzU}P9vV&NUrgqJzolcc9lqzlvQ@Yw9tLA(EKFrfR@($`{97#O*+17PNPHZO zXXdqB7>0+ruyh?4OqTX_&iGX#Xf6L(uchHqeru}k;^KH%B&T9$JWOH#-GJr<2*8F6 z=#Ps`l-bjb3Ua^%(QL*RAz13RD86uT0_fzKn3zD-t*Yl_FrG_*(X8Jd#mqMmCa~pS z+XQ>unh0iAeo%4Lcu-wqrs^V&Q|E#)J)1YgOkvkv6s3 zC&5Ro<-2ZtVG=l7yl!dmY8=&>c2}$Jt~2g#x8C$IkjcUxbOV0z;EgNBfDScdDfp4j zE^1KMIfb$z-*l|F$<*fhfi`3cs|&OVuBa*m zPs~ptH*y4reQ|Vz>`{|O&QB03H{taZn9S;7}%+7t~&??;+P|Q^E^XY&Qu$0X6^Zk z&^ZZ`7)-YbE|`~2$eY3*NJl!vv6nM!h!zuyWjj1&qT)|h^*Wt~Dn)DYdf5$kOr=?D z$)J*(*7krM*U2uJPXmYHnZ)VW+qh6P!)_aGu(###K|7jyE$Xs}d9cOCxS{?t zS*>L93FoSh_U7!cGmD;ucNc81E3Ph}K9*%uhd=1#@L=4TP3<{s6<%f2J3#(kVG45~ zNv^~C9GJ`YKI2#f#!aSnYH&kB0I%zRH6Yta31C<7|Cu&L{f=Lpd15TckqQY4p0iIJ^hqH-ae>_pgzx=L7b1 zq@olvih1!!p=@3lMPt zQQ9mjyrMyYn;V$&?Z*09;LpY%RN%}K@W34hnS)~DQbH!9j@YJ1y9!@$|}$p5{$OGAfSW<$1o6Cclp`K-FV3s>R7IdndEI=TVB8&vrHIW&$BulGWS zVE$Z+vi5c_)EPT&GdpbN)Q>L(g^Nq+ZRJ`DCbvQ;Xb2N6&JLK=i+CG>Lq>palG9+iMX(SJS}%YB99jWK;mK{Qm1L(P0} zg1bZpxXSJ|Wz?2)T;0Xqh71w+j%}slrgy~wJIg@l{ENF6E-9mH2^|S1(MZ?fU)|2s z&F^bl>8A8Qx}64heH&@rTjhd}${lJFN9_;Z@IcV(y I4M5@l5A)DRe*gdg From d54f163985b65158a3c1a1d0f9717f0d669e6724 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Fri, 16 Jun 2017 17:05:43 +0800 Subject: [PATCH 66/67] tweak c program build --- tools/build.sh | 10 ++++++++++ tools/src/CMakeLists.txt | 27 +++++++++++++++++++++++++++ tools/{ => src}/Makefile | 0 tools/{ => src}/read.c | 3 +++ 4 files changed, 40 insertions(+) create mode 100755 tools/build.sh create mode 100644 tools/src/CMakeLists.txt rename tools/{ => src}/Makefile (100%) rename tools/{ => src}/read.c (97%) diff --git a/tools/build.sh b/tools/build.sh new file mode 100755 index 0000000..c7eb31c --- /dev/null +++ b/tools/build.sh @@ -0,0 +1,10 @@ +# @Author: mcxiaoke +# @Date: 2017-06-16 17:07:06 +# @Last Modified by: mcxiaoke +# @Last Modified time: 2017-06-16 17:11:47 +#!/usr/bin/env bash +cd src +make && make install && make clean +cd .. +packer +exit diff --git a/tools/src/CMakeLists.txt b/tools/src/CMakeLists.txt new file mode 100644 index 0000000..3ede263 --- /dev/null +++ b/tools/src/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required (VERSION 2.6) +project(packer) + +set(VER_MAJOR 2) +set(VER_MINOR 0) +set(VER_PATCH 0) + +include (CheckFunctionExists) + +configure_file ( + "${PROJECT_SOURCE_DIR}/Config.h.in" + "${PROJECT_BINARY_DIR}/Config.h" + ) + +include_directories("${PROJECT_BINARY_DIR}") + +aux_source_directory(. SOURCE) +# add_subdirectory(math) +add_executable(packer ${SOURCE}) +# target_link_libraries(packer mathlib) + +# in sub dir CMakeLists.txt +# aux_source_directory(. DIR_LIB_SRCS) +# add_library (mathlib ${DIR_LIB_SRCS}) + +install (TARGETS packer DESTINATION bin) +# install (FILES "${PROJECT_BINARY_DIR}/Config.h" DESTINATION include) \ No newline at end of file diff --git a/tools/Makefile b/tools/src/Makefile similarity index 100% rename from tools/Makefile rename to tools/src/Makefile diff --git a/tools/read.c b/tools/src/read.c similarity index 97% rename from tools/read.c rename to tools/src/read.c index e01a5cf..b98b944 100644 --- a/tools/read.c +++ b/tools/src/read.c @@ -4,7 +4,9 @@ * @Last Modified by: mcxiaoke * @Last Modified time: 2017-06-13 18:23:41 */ +//#include "config.h" #include +#include #include #include #include @@ -115,6 +117,7 @@ int main(int argc, char *argv[]) { size_t length; if (argc < 2) { + // printf("Version: %d.%d.%d\n", VER_MAJOR, VER_MINOR, VER_PATCH); printf("Usage: %s app.apk (show apk channel)\n", argv[0]); exit(EXIT_FAILURE); } From 80ac0ff3e6fc040d22d039fcfa21e52954d38871 Mon Sep 17 00:00:00 2001 From: mcxiaoke Date: Thu, 22 Jun 2017 14:07:16 +0800 Subject: [PATCH 67/67] release and upload v2.0.0 --- app/build.gradle | 2 +- gradle.properties | 4 ++-- readme.md | 2 +- ...acker-ng-1.9.3.jar => packer-ng-2.0.0.jar} | Bin 224453 -> 224451 bytes tools/packer-ng-v2.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename tools/{packer-ng-1.9.3.jar => packer-ng-2.0.0.jar} (94%) diff --git a/app/build.gradle b/app/build.gradle index 6c120c7..7935828 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.packer_version = '1.9.3-SNAPSHOT' + ext.packer_version = '2.0.0-SNAPSHOT' repositories { maven { url '/tmp/repo/' } diff --git a/gradle.properties b/gradle.properties index 19f3ecb..f244c91 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -VERSION_NAME=1.9.3 -VERSION_CODE=110 +VERSION_NAME=2.0.0 +VERSION_CODE=200 GROUP=com.mcxiaoke.packer-ng diff --git a/readme.md b/readme.md index c572f2a..fdbfad8 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ PackerNg V2 ======== 极速渠道打包工具 -- **v2.0.0 - 2017.06.30** - 全新发布,支持V2签名模式,包含多项优化 +- **v2.0.0 - 2017.06.23** - 全新发布,支持V2签名模式,包含多项优化 ## 特别提示 diff --git a/tools/packer-ng-1.9.3.jar b/tools/packer-ng-2.0.0.jar similarity index 94% rename from tools/packer-ng-1.9.3.jar rename to tools/packer-ng-2.0.0.jar index 7454aa4644d4a5162c60aa80b55150515fd29165..15d310836cac3f0cfd80827dac7c71cd19c81341 100644 GIT binary patch delta 3478 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 KGp`_^<^K<}cTF$= delta 3559 zcmZuzc~n%_8GrA72m-@6vJay;t_VdL7#P-JW=ic9PzxUnmF7KVZ z?s4k6heu8ZIJpTz|NcV$rx!MQjB=$W{a1cRUme_-F!bXL_Y(%q>L<0otbGk>D}Mji zw44EMiRvHCYubu7?;Lu4o8?x^=)}_OlhLil8r_4{jaS9VJ5B!n4{NuD4Bd)nCX?;x47{+laA@y>p=8>pP*-e5H_HjudVqznIS z{*e~=Gao@1AuaUv!_*qk?9bQ0V>j8FqJuxH>8)QN(Z!_~CjZc*=P-89~Rc*x9Q_kQWu_ zXMonJ)3(He;s%DklWw2z!}^48;{!&#H^8wsTm>Q8F+dQu*S_QfV)1b#sN{5BMN&G! zUPO+?lqk^3)9KkYf)Lhc_%vT~2I0;q@Rf%jjiTW>BX|;*1+$NU7)pe7bn15}QwYJ_99N%%AdV%dshY2z** zw8cUQ+spg@7#0UExIPxbnKq?|xETx4tUa@v$Y91v-F+{7`$%-g+gbFc!*LM6Hsy6o z&eHijXM7z;(HAZ2A@?jZ;?j5`Km4dSsmHtVU|`Qz_9k7?oB-ok(kH!17c5AILHKDB z_+onkz4F(q`pP;p3r8!(BF781W3p*kyw( z)^bkPx~`0p-k$Tqh9vSIzhdW%9BZVsc8z28SJs-_9nq+YIV|Fa15??wxF{I{(0v>w z+IqWNkrv!lVaYg%VZnEMah}Sm-{awY(On+i5|AW8w2H0Y1dQmMF2CI?f- zj>I~E9&e=4v~tZ3rav~A#ew)Rp5E$f?ekVQtHb4j8`8lbTJTspR5GtDE@&|&OB}@W z4PjTZabY$DVlxMt91e!zOAc~mFcMQI5|Hd*X(kN7T@xXK9m?h2ktj2oOh{&VQyq+j zouAIZ7|hP1^ZXlfHEA*D>bRr)%^eMy521OgYK~(W;K!a;0(L&Z?(|J087D(}ag%VIIZ&th28+7@u^~a>_U69d0=Brym-&hO3JHOaM#?B&+Oyjk-&o0XXgZm82Qw7Imgi^u0o(w$PI~wJ(MSg z7D60Yi+fY`#Y~L>)u2Ls5)JygkQRP2UMhr{&@{KlsKVkRFoDye9?A)=iF?u7Uq3K&vGAD$J>3OLlv+|bR`J)Lc3 zv_U59ReZ!R)Tt6Ff#VdGg zHeG9V=M?wca=O;qFDf8x4%MK&oObf9OAaQNt-7Xwqt}=ZE-xp~_j4$pM^7-P0zyzz z0g2)z%&veLaO*9T_T_ow-b(0)-;yf&;nND3%(Nmtt%M<1UI~$`5I7itM=R;@Z^y1m zn97(7=K|2=LPz~AzlG(+$Qe(I(IIO-c(4k_GSB|p=!0*nU^M@K#SImp?bm5L>k86J z(FY5wK_l}EszC?frm)+pVUWxo=d7n%VP8~3;E?xzp}W73)NZf!!|)m~4&|ogRp(TT z>C+bGF9-n55Feq%jf%e7jxu9_#9sv~hf8XzDW&i*H diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py index f98bda3..d74189b 100644 --- a/tools/packer-ng-v2.py +++ b/tools/packer-ng-v2.py @@ -2,7 +2,7 @@ # @Author: mcxiaoke # @Date: 2017-06-06 14:03:18 # @Last Modified by: mcxiaoke -# @Last Modified time: 2017-06-12 15:06:25 +# @Last Modified time: 2017-06-22 17:26:29 from __future__ import print_function import os import sys