diff --git a/.gitignore b/.gitignore index 052524a..827146f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,16 @@ build/ apks/ repo/ +dist/ +tmp/ *.iml *.apk *.pyc +*.d +*.o +*.class .DS_Store -packer.properties +a.out +.classpath +.project +.settings/ 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/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..7935828 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,117 @@ +buildscript { + ext.packer_version = '2.0.0-SNAPSHOT' + + repositories { + maven { url '/tmp/repo/' } + jcenter() + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } + } + + dependencies { + classpath "com.android.tools.build:gradle:2.2.3" + classpath "com.mcxiaoke.packer-ng:plugin:$packer_version" + } +} + +repositories { + maven { url '/tmp/repo/' } + jcenter() + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } +} + +apply plugin: 'com.android.application' +apply plugin: 'packer' + +// https://code.google.com/p/android/issues/detail?id=171089 +dependencies { + compile "com.mcxiaoke.packer-ng:helper:$packer_version" +} + +//packer-begin +packer { + archiveNameFormat = '${appPkg}-${buildType}-v${versionName}-${channel}' + archiveOutput = new File(project.rootProject.buildDir, "apks") +// channelList = ['*Douban*', 'Google/', '中文/@#市场', 'Hello@World', +// 'GradleTest', '20070601!@#$%^&*(){}:"<>?-=[];\',./'] +// channelFile = new File(project.rootDir, "channels.txt") + channelMap = [ + "free" : project.rootProject.file("channels/free.txt"), + "paid" : project.rootProject.file("channels/paid.txt"), + "other": project.rootProject.file("channels/channels.txt") + ] +} +//packer-end + +android { + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + encoding "UTF-8" + } + + compileSdkVersion project.compileSdkVersion + buildToolsVersion project.buildToolsVersion + + defaultConfig { + versionName project.VERSION_NAME + versionCode Integer.parseInt(project.VERSION_CODE) + minSdkVersion project.minSdkVersion + targetSdkVersion project.targetSdkVersion + } + + signingConfigs { + v2 { + storeFile file("android.keystore") + storePassword "android" + keyAlias "android" + keyPassword "android" + v2SigningEnabled true + } + + v1 { + storeFile file("android.keystore") + storePassword "android" + keyAlias "android" + keyPassword "android" + v2SigningEnabled false + } + + } + + buildTypes { + release { + signingConfig signingConfigs.v2 + minifyEnabled false + } + + beta { + signingConfig signingConfigs.v1 + minifyEnabled false + } + + alpha { + minifyEnabled false + } + + } + + productFlavors { + free {} + + paid {} + + other {} + } + + lintOptions { + abortOnError false + htmlReport true + } + + packagingOptions { + exclude 'LICENSE.txt' + exclude 'META-INF/services/javax.annotation.processing.Processor' + } + +} 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/app/gradle.properties b/app/gradle.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/gradle.properties @@ -0,0 +1 @@ + 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 68% rename from sample/src/main/AndroidManifest.xml rename to app/src/main/AndroidManifest.xml index e0daecf..00687dd 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,22 +1,20 @@ + package="com.mcxiaoke.packer.samples"> - + android:label="PackerNg"> + android:name=".MainActivity" + android:label="PackerNg"> 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/app/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java b/app/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java new file mode 100644 index 0000000..2cebf08 --- /dev/null +++ b/app/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java @@ -0,0 +1,43 @@ +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 = "PackerNg"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + 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)); + + 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.getChannel(new File(app.sourceDir))); + } + } + + } + +} 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/build.gradle b/build.gradle index e69de29..76cc5ef 100644 --- a/build.gradle +++ b/build.gradle @@ -0,0 +1,9 @@ +ext { + compileSdkVersion = 25 + buildToolsVersion = "25.0.3" + minSdkVersion = 14 + targetSdkVersion = 22 +} + +group = GROUP +version = VERSION_NAME 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/cli/build.gradle b/cli/build.gradle new file mode 100644 index 0000000..b3b35f1 --- /dev/null +++ b/cli/build.gradle @@ -0,0 +1,49 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + } +} + +repositories { + jcenter() +} + +apply plugin: 'java' +//apply plugin: 'application' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +dependencies { + compile project(":common") + 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', + 'Implementation-Version': VERSION_NAME, + 'Main-Class': 'com.mcxiaoke.packer.cli.Main', + 'Description': 'This is PackerNg 2 executable Jar.', + 'Owner': 'packer-ng-plugin@mcxiaoke.com', + 'Project': 'https://github.com/mcxiaoke/packer-ng-plugin') + } + baseName = 'packer-ng' + +} + +task distJar(type: Copy, dependsOn: fatJar) { + from fatJar.outputs.files + into project.rootProject.file('tools') +} + +// 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/Bridge.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java new file mode 100644 index 0000000..3cdb3da --- /dev/null +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Bridge.java @@ -0,0 +1,47 @@ +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.PackerCommon; + +import java.io.File; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + +/** + * User: mcxiaoke + * Date: 2017/5/26 + * Time: 16:21 + */ +public class Bridge { + + public static void writeChannel(File file, String channel) throws IOException { + PackerCommon.writeChannel(file, channel); + } + + public static String readChannel(File file) throws IOException { + return PackerCommon.readChannel(file); + } + + public static boolean verifyChannel(File file, String channel) throws IOException { + return verifyApk(file) && (channel.equals(readChannel(file))); + } + + 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(); + } catch (ApkFormatException e) { + throw new IOException(e); + } catch (NoSuchAlgorithmException e) { + throw new IOException(e); + } + + } + +} 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..9dace2c --- /dev/null +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Helper.java @@ -0,0 +1,146 @@ +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.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * User: mcxiaoke + * Date: 2017/5/31 + * Time: 16:52 + */ + +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 ch = parts[0].trim(); + if (ch.length() > 0) { + channels.add(ch); + } + } + } + br.close(); + fr.close(); + 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 { + 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 new file mode 100644 index 0000000..9b13c66 --- /dev/null +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Main.java @@ -0,0 +1,175 @@ +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.Collection; +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 ("generate".equals(cmd)) { + generate(params); + } else if ("verify".equals(cmd)) { + verify(params); + } else if ("help".equals(cmd)) { + printUsage(); + } else if ("version".equals(cmd)) { + printUsage(); + } else { + System.err.println( + "Unsupported command: " + cmd); + printUsage(); + } + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + } + + public static void printUsage() { + Helper.printUsage(); + } + + private static void generate(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) + Collection 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("@")) { + channels = Helper.parseChannels(new File(value.substring(1))); + } else { + channels = Helper.parseChannels(value); + } + } 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 { + System.err.println( + "Unsupported option: " + form); + printUsage(); + } + } + 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); + } + doGenerate(apkFile, channels, outputDir); + } + + private static void doGenerate(File apkFile, Collection channels, File outputDir) + throws IOException { + if (apkFile == null + || !apkFile.exists() + || !apkFile.isFile()) { + throw new IOException("Invalid Input APK: " + apkFile); + } + if (!Bridge.verifyApk(apkFile)) { + throw new IOException("Invalid Signature: " + apkFile); + } + if (outputDir.exists()) { + Helper.deleteAPKs(outputDir); + } else { + outputDir.mkdirs(); + } + System.out.println("Input: " + apkFile.getAbsolutePath()); + System.out.println("Output:" + outputDir.getAbsolutePath()); + System.out.println("Channels:" + Arrays.toString(channels.toArray())); + 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); + 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); + } + } + } + + 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 = Bridge.verifyApk(apkFile); + final String channel = Bridge.readChannel(apkFile); + System.out.println("File: " + apkFile.getName()); + System.out.println("Signed: " + verified); + System.out.println("Channel: " + channel); + } + + +} diff --git a/cli/src/main/java/com/mcxiaoke/packer/cli/Options.java b/cli/src/main/java/com/mcxiaoke/packer/cli/Options.java new file mode 100644 index 0000000..df299fc --- /dev/null +++ b/cli/src/main/java/com/mcxiaoke/packer/cli/Options.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 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 Options(String[] params) { + this.params = 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 (index >= params.length) { + // No more parameters left + return null; + } + String param = params[index]; + if (!param.startsWith("-")) { + // Not an option + return null; + } + + index++; + lastOptionOriginalForm = param; + lastOptionValue = 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) { + lastOptionValue = param.substring(valueDelimiterIndex + 1); + lastOptionOriginalForm = 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 lastOptionOriginalForm; + } + + /** + * Returns the value of the current option, throwing an exception if the value is missing. + */ + public String getRequiredValue(String valueDescription) throws OptionsException { + if (lastOptionValue != null) { + String result = lastOptionValue; + lastOptionValue = null; + return result; + } + if (index >= params.length) { + // No more parameters left + throw new OptionsException( + valueDescription + " missing after " + lastOptionOriginalForm); + } + String param = params[index]; + if ("--".equals(param)) { + // End of options marker + throw new OptionsException( + valueDescription + " missing after " + lastOptionOriginalForm); + } + index++; + 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 + " (" + lastOptionOriginalForm + + ") 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 (lastOptionValue != null) { + // --option=value form + String stringValue = lastOptionValue; + lastOptionValue = null; + if ("true".equals(stringValue)) { + return true; + } else if ("false".equals(stringValue)) { + return false; + } + throw new OptionsException( + "Unsupported value for " + lastOptionOriginalForm + ": " + stringValue + + ". Only true or false supported."); + } + + // --option (true|false) form OR just --option + if (index >= params.length) { + return defaultValue; + } + + String stringValue = params[index]; + if ("true".equals(stringValue)) { + index++; + return true; + } else if ("false".equals(stringValue)) { + index++; + 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 (index >= params.length) { + return new String[0]; + } + String param = params[index]; + if ("--".equals(param)) { + // Skip end of options marker + return Arrays.copyOfRange(params, index + 1, params.length); + } else { + return Arrays.copyOfRange(params, index, params.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/resources/com/mcxiaoke/packer/cli/help.txt b/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt new file mode 100644 index 0000000..a661e86 --- /dev/null +++ b/cli/src/main/resources/com/mcxiaoke/packer/cli/help.txt @@ -0,0 +1,32 @@ + +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 generate --channels --output apk + packer-ng verify apk + +EXAMPLE + +generate Add channel info to the provided 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. + --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/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000..f8f0182 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,37 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + } +} + +repositories { + jcenter() +} + +apply plugin: 'java' + +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/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/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..9030e9e --- /dev/null +++ b/common/src/main/java/com/mcxiaoke/packer/common/PackerCommon.java @@ -0,0 +1,198 @@ +package com.mcxiaoke.packer.common; + +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.util.Arrays; +import java.util.HashMap; +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"; + // 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); + Support.writeBlock(file, blockId, buffer); + } + + // package visible for test + static byte[] readPayloadImpl(File file, int blockId) + throws IOException { + ByteBuffer buffer = Support.readBlock(file, blockId); + if (buffer == null) { + return null; + } + byte[] magic = BLOCK_MAGIC.getBytes(UTF8); + byte[] actual = new byte[magic.length]; + buffer.get(actual); + 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; + } + + // 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 + @-4 payload length same as @+16 4 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.flip(); + return buffer; + } + + public 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(); + } + + public 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..73551c0 --- /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; + +final class PayloadReader { + private PayloadReader() { + super(); + } + + public static byte[] readBytes(final File apkFile, final int id) + throws IOException { + final ByteBuffer buf = readBlock(apkFile, id); + return buf == null ? null : V2Utils.getBytes(buf); + } + + public static ByteBuffer readBlock(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..85703ec --- /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; + + +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/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/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..f8c2936 --- /dev/null +++ b/common/src/test/java/com/mcxiaoke/packer/common/PayloadTests.java @@ -0,0 +1,229 @@ +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.Support; +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("../tools/test.apk"); + assertTrue(file.exists()); + } + + public void testFileCopy() throws IOException { + File f1 = new File("../tools/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 + 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(); + 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(); + Support.writeBlock(f, 0x12345, in); + byte[] out = Support.readBytes(f, 0x12345); + assertTrue(TestUtils.sameBytes(in, out)); + checkApkVerified(f); + } + + public void testBytesWrite2() throws IOException { + File f = newTestFile(); + byte[] in = "中文和特殊符号测试!@#¥%……*()《》?:【】、".getBytes("UTF-8"); + Support.writeBlock(f, 0x12345, in); + byte[] out = Support.readBytes(f, 0x12345); + assertTrue(TestUtils.sameBytes(in, out)); + checkApkVerified(f); + } + + public void testStringWrite() throws IOException { + File f = newTestFile(); + PackerCommon.writeString(f, "Test String", 0x717a786b); + assertEquals("Test String", PackerCommon.readString(f, 0x717a786b)); + PackerCommon.writeString(f, "中文和特殊符号测试!@#¥%……*()《》?:【】、", 0x717a786b); + assertEquals("中文和特殊符号测试!@#¥%……*()《》?:【】、", PackerCommon.readString(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"); + PackerCommon.writeValues(f, in, 0x12345); + Map out = PackerCommon.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"); + 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", 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); + } + + 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); + Support.writeBlock(f, 0x123456, in); + ByteBuffer out = Support.readBlock(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(); + 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/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..435b04d --- /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("../tools/"); + File file = new File(dir, "test.apk"); + File tf = new File(dir, System.currentTimeMillis() + "-test.apk"); + FileUtils.copyFile(file, tf); + return tf; + } + + 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(); + } +} diff --git a/gradle.properties b/gradle.properties index 29e4ff5..f244c91 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,9 @@ -VERSION_NAME=1.0.9 -VERSION_CODE=109 +VERSION_NAME=2.0.0 +VERSION_CODE=200 -GROUP=com.mcxiaoke.gradle +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,9 +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 - -MIN_SDK_VERSION=15 -TARGET_SDK_VERSION=24 -COMPILE_SDK_VERSION=24 -BUILD_TOOLS_VERSION=24.0.1 +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/helper/build.gradle b/helper/build.gradle index 380e303..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.6 -targetCompatibility = 1.6 +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/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/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..19b7a6f 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,11 @@ 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.PackerCommon; + 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 @@ -26,435 +13,30 @@ * 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 sCachedMarket; - - public static String getMarket(final Object context) { - return getMarket(context, EMPTY_STRING); - } - - public static synchronized String getMarket(final Object context, final String defaultValue) { - if (sCachedMarket == null) { - sCachedMarket = getMarketInternal(context, defaultValue).market; - } - return sCachedMarket; - } - - public static MarketInfo getMarketInfo(final Object context) { - return getMarketInfo(context, EMPTY_STRING); - } - - public static synchronized MarketInfo getMarketInfo(final Object context, final String defaultValue) { - return getMarketInternal(context, defaultValue); - } + private static String sCachedChannel; - private static MarketInfo getMarketInternal(final Object context, final String defaultValue) { - String market; - Exception error; + public static String getChannel(final File file) { try { - final String sourceDir = Helper.getSourceDir(context); - market = Helper.readMarket(new File(sourceDir)); - error = null; + return PackerCommon.readChannel(file); } catch (Exception e) { - market = null; - error = e; + return EMPTY_STRING; } - return new MarketInfo(market == null ? defaultValue : market, error); } - public static final class MarketInfo { - public final String market; - public final Exception error; - - public MarketInfo(final String market, final Exception error) { - this.market = market; - this.error = error; - } - - @Override - public String toString() { - return "MarketInfo{" + - "market='" + market + '\'' + - ", 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; - } + public static String getChannel(final Context context) { + try { + return getChannelOrThrow(context); + } catch (Exception e) { + return EMPTY_STRING; } } - 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); - } + public static synchronized String getChannelOrThrow(final Context context) + throws IOException { + final ApplicationInfo info = context.getApplicationInfo(); + return PackerCommon.readChannel(new File(info.sourceDir)); } } 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/plugin/build.gradle b/plugin/build.gradle index 7f36cab..b072320 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -1,7 +1,15 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + } +} + apply plugin: 'groovy' -sourceCompatibility = 1.6 -targetCompatibility = 1.6 +sourceCompatibility = 1.7 +targetCompatibility = 1.7 buildscript { repositories { @@ -16,8 +24,10 @@ repositories { dependencies { compile localGroovy() compile gradleApi() - compile project(':helper') - compile 'com.android.tools.build:gradle:2.2.1' + compile project(':common') + compile project(':cli') + 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/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 14362df..0000000 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/ArchiveAllApkTask.groovy +++ /dev/null @@ -1,174 +0,0 @@ -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 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 -import java.util.jar.JarEntry -import java.util.jar.JarFile - -/** - * 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!") - } - - // 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) { - throw new GradleException(":${name} " + - "apk ${apkPath} not 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 { - PackerNg.Helper.writeMarket(tempFile, market) - String apkName = buildApkName(theVariant, market, tempFile) - File finalFile = new File(outputDir, apkName) - if (PackerNg.Helper.verifyMarket(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 62% 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..f0cab9a 100644 --- a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanArchivesTask.groovy +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/CleanTask.groovy @@ -9,18 +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') - } - - @TaskAction - void showMessage() { - logger.info("${name}: ${description}") + CleanTask() { + description = 'clean all files in output dir' } @TaskAction 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..137bc18 --- /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 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" + + static final String DEFAULT_OUTPUT = "archives" // in build dir + + /* + * file name template string + * + * Available vars: + * 1. projectName + * 2. appName + * 3. appPkg + * 4. channel + * 5. buildType + * 6. versionName + * 7. versionCode + * 8. buildTime + * 9. 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 new file mode 100644 index 0000000..1e90dc5 --- /dev/null +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleExtension.groovy @@ -0,0 +1,20 @@ +package com.mcxiaoke.packer.ng + +class GradleExtension { + File archiveOutput + String archiveNameFormat + Set channelList; + File channelFile; + Map channelMap; + + @Override + String toString() { + return "{" + + "archiveOutput=" + archiveOutput + + "\narchiveNameFormat='" + archiveNameFormat + '\'' + + "\nchannelList=" + channelList + + "\nchannelFile=" + channelFile + + "\nchannelMap=" + channelMap + + '}'; + } +} diff --git a/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy new file mode 100644 index 0000000..b9cd879 --- /dev/null +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradlePlugin.groovy @@ -0,0 +1,78 @@ +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 + +// Android PackerNg Plugin Source +class GradlePlugin 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 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) + project.afterEvaluate { + project.android.applicationVariants.all { BaseVariant variant -> + addTasks(variant) + } + } + } + + 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("addTasks() for ${vt.name}") + def variantTask = project.task("apk${vt.name.capitalize()}", + type: GradleTask) { + variant = vt + extension = project.packer + dependsOn vt.assemble + } + + debug("addTasks() new variant task:${variantTask.name}") + + def buildTypeName = vt.buildType.name + if (vt.name != buildTypeName) { + def taskName = "apk${buildTypeName.capitalize()}" + def task = project.tasks.findByName(taskName) + if (task == null) { + task = project.task(taskName) + } + task.dependsOn(variantTask) + debug("addTasks() build type task ${taskName}") + } + + } + + void debug(String msg) { + project.logger.info(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 new file mode 100644 index 0000000..1cbb545 --- /dev/null +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/GradleTask.groovy @@ -0,0 +1,223 @@ +package com.mcxiaoke.packer.ng + +import com.android.build.gradle.api.BaseVariant +import com.mcxiaoke.packer.cli.Bridge +import com.mcxiaoke.packer.cli.Helper +import groovy.text.SimpleTemplateEngine +import groovy.text.Template +import org.gradle.api.DefaultTask +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 GradleTask extends DefaultTask { + + @Input + BaseVariant variant + + @Input + GradleExtension extension + + GradleTask() { + description = 'generate APK with channel info' + } + + Template getNameTemplate() { + String format + String propValue = project.findProperty(Const.PROP_FORMAT) + 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) + } + + 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}'") + } + return file + } + + File getOutputRoot() { + 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.rootProject.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) + } + if (!outputDir.exists()) { + outputDir.mkdirs() + } else { + logger.info(":${name} delete old APKs in ${outputDir.absolutePath}") + Helper.deleteAPKs(outputDir) + } + return outputDir + } + + Set getChannels() { + // -P channels=ch1,ch2,ch3 + // -P channels=@channels.txt + // channelList = [ch1,ch2,ch3] + // channelFile = project.file("channels.txt") + Collection channels = [] + // 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 PluginException("channel file not exists: '${f.absolutePath}'") + } + channels = Helper.parseChannels(f) + } else { + throw new PluginException("invalid channels property: '${prop}'") + } + } else { + channels = Helper.parseChannels(prop); + } + if (channels == null || channels.isEmpty()) { + throw new PluginException("invalid channels property: '${prop}'") + } + return channels + } + if (extension.channelList != null) { + channels = Helper.escape(extension.channelList) + logger.info(":${project.name} ext.channelList: ${channels}") + } 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 = Helper.parseChannels(f) + } + } else if (extension.channelFile != null) { + File f = extension.channelFile + logger.info(":${project.name} extension.channelFile: ${f}") + if (!f.isFile()) { + throw new PluginException("channel file not exists: '${f.absolutePath}'") + } + channels = Helper.parseChannels(f) + } + if (channels == null || channels.isEmpty()) { + throw new PluginException("No channels found") + } + return channels + } + + + void showProperties() { + 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("PackerNg - https://github.com/mcxiaoke/packer-ng-plugin") + println("============================================================") + showProperties() + 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(' ')}]") + for (String channel : channels) { + File tempFile = new File(outputDir, "tmp-${channel}.apk") + 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}") + tempFile.renameTo(finalFile) + logger.info("Generated: ${finalFile}") + } else { + throw new PluginException("${channel} APK verify failed") + } + } catch (IOException ex) { + throw new PluginException("${channel} APK generate failed", ex) + } finally { + tempFile.delete() + } + } + println("Outputs: ${rootDir.absolutePath}") + println("============================================================") + } + + String buildApkName(channel, file, template) { + def buildTime = new SimpleDateFormat('yyyyMMdd-HHmmss').format(new Date()) + def fileSHA1 = HASH.sha1(file) + def nameMap = [ + 'appName' : project.name, + 'projectName': project.rootProject.name, + 'fileSHA1' : fileSHA1, + 'channel' : channel, + 'flavor' : variant.flavorName, + 'buildType' : variant.buildType.name, + 'versionName': variant.versionName, + 'versionCode': variant.versionCode, + '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/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/groovy/com/mcxiaoke/packer/ng/PluginException.groovy b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PluginException.groovy new file mode 100644 index 0000000..a51b5da --- /dev/null +++ b/plugin/src/main/groovy/com/mcxiaoke/packer/ng/PluginException.groovy @@ -0,0 +1,23 @@ +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}") + super() + } + + PluginException(final String message) { + super(message) + } + + PluginException(final String message, final Throwable cause) { + super(message, 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; + } + } +} 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..3521d98 --- /dev/null +++ b/plugin/src/main/resources/META-INF/gradle-plugins/packer.properties @@ -0,0 +1 @@ +implementation-class=com.mcxiaoke.packer.ng.GradlePlugin \ 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 { - -} diff --git a/readme.md b/readme.md index 974ad83..fdbfad8 100644 --- a/readme.md +++ b/readme.md @@ -1,29 +1,16 @@ -下一代Android渠道打包工具 +PackerNg V2 ======== +极速渠道打包工具 -## 特别提示 - -- **请使用最新版本的PackerNg,如果使用的Android Gradle Plugin版本大于2.2.0,请务必在 `signingConfigs` 里增加 `v2SigningEnabled false` 禁用新版签名模式,详细的说明见这里:[兼容性问题说明](compatibility.md)。** +- **v2.0.0 - 2017.06.23** - 全新发布,支持V2签名模式,包含多项优化 -- **如果你同时使用 [tinker](https://github.com/Tencent/tinker) ,请使用`1.0.9`以后的版本,同时阅读tinker的文档,确保没有兼容问题。** - -## 最新版本 +## 特别提示 -- **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版只支持`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)。 ## 项目介绍 -[**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系统集成,同时提供命令行打包脚本,渠道读取提供Python和C语言的实现。 ## 使用指南 @@ -34,11 +21,9 @@ ```groovy buildscript { - ...... - dependencies{ - // add packer-ng - classpath 'com.mcxiaoke.gradle:packer-ng:1.0.9' - } + dependencies{ + classpath 'com.mcxiaoke.packer-ng:plugin:2.0.0' + } } ``` @@ -48,194 +33,171 @@ buildscript { apply plugin: 'packer' dependencies { - compile 'com.mcxiaoke.gradle:packer-helper:1.0.9' + compile 'com.mcxiaoke.packer-ng:helper:2.0.0' } +``` - android { - //... - signingConfigs { - release { - // 满足下面两个条件时需要此配置 - // 1. Gradle版本 >= 2.14.1 - // 2. Android Gradle Plugin 版本 >= 2.2.0 - // 作用是只使用旧版签名,禁用V2版签名模式 - v2SigningEnabled false - } - } - } +**注意:`plugin` 和 `helper` 的版本号需要保持一致** + +### 插件配置示例 + +```groovy +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-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)); +渠道名建议尽量使用规范的**中英文和数字**,不要使用特殊字符和不可见字符。示例:[channels.txt](blob/v2dev/channels/channels.txt) -``` +### 集成打包 + +* 项目中没有使用 `productFlavors` -### Gradle打包说明 + ```shell + ./gradlew clean apkRelease + ``` -可以通过两种方式指定 `market` 属性,根据需要选用: +* 项目中使用了 `productFlavors` -- 打包时命令行使用 `-Pmarket= yourMarketFilePath` 指定属性 -- 在 `gradle.properties` 里加入 `market=yourMarketFilePath` + 如果项目中指定了多个 `flavor` ,需要指定需要打渠道包的 `flavor` 名字,假设你有 `Paid` `Free` 两个 `flavor` ,打包的时候命令如下: -market是你的渠道名列表文件,market文件是基于**项目根目录**的 `相对路径` ,假设你的项目位于 `~/github/myapp` 你的market文件位于 `~/github/myapp/config/markets.txt` 那么参数应该是 `-Pmarket=config/markets.txt`,一般建议直接放在项目根目录,如果market文件参数错误或者文件不存在会抛出异常。 + ```shell + ./gradlew clean apkPaidRelease + ./gradlew clean apkFreeRelease + ``` + + 直接使用 `./gradlew clean apkRelease` 会输出所有 `flavor` 的渠道包。 -渠道名列表文件是纯文本文件,每行一个渠道号,列表解析的时候会自动忽略空白行和格式不规范的行,请注意看命令行输出,渠道名和注释之间用 `#` 号分割开,可以没有注释,示例: +* 通过参数直接指定渠道列表(会覆盖`build.gradle`中的属性): -``` - Google_Play#play store market - Gradle_Test#test - SomeMarket#some market - HelloWorld -``` + ```shell + ./gradlew clean apkRelease -Pchannels=ch1,ch2,douban,google + ``` + + 渠道数目很少时可以使用此种方式。 -渠道打包的Gradle命令行参数格式示例(在项目根目录执行): +* 通过参数指定渠道列表文件的位置(会覆盖`build.gradle`中的属性): -```shell -./gradlew -Pmarket=markets.txt clean apkRelease -``` + ```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打包命令说明 -渠道打包的Gradle Task名字是 `apk${buildType}` buildType一般是release,也可以是你自己指定的beta或者someOtherType,使用时首字母需要大写,例如release的渠道包任务名是 `apkRelease`,beta的渠道包任务名是 `apkBeta`,其它的以此类推。 + 渠道打包的Task名字是 `apk${flavor}${buildType}` buildType一般是release,也可以是你自己指定的beta或者someOtherType,如果没有 `flavor` 可以忽略,使用时首字母需要大写,假设 `flavor` 是 `Paid`,`release`类型对应的任务名是 `apkPaidRelease`,`beta`类型对应的任务名是 `apkPaidBetaBeta`,其它的以此类推。 + +* 特别提示 -#### 注意事项 + 如果你同时使用其它的资源压缩工具或应用加固功能,请使用命令行脚本打包增加渠道信息,增加渠道信息需要放在APK处理过程的最后一步。 + +### 脚本打包 -**不支持`productFlavors`中定义的条件编译变量,不支持修改AndroidManifest** +除了使用Gradle集成以外,还可以使用项目提供的Java脚本打包,Jar位于本项目的 `tools` 目录,请使用最新版,以下用 `packer-ng` 指代 `java -jar tools/packer-ng-2.0.0.jar`,示例: -如果你的项目有多个`productFlavors`,默认只会用第一个`flavor`生成的APK文件作为打包工具的输入参数,忽略其它`flavor`生成的apk,代码里用的是 `theVariant.outputs[0].outputFile`。如果你想指定使用某个flavor来生成渠道包,可以用 `apkFlavor1Release`,`apkFlavor2Beta`这样的名字,示例(假设flavor名字是Intel): +* 直接指定渠道列表打包: ```shell -./gradlew -Pmarket=markets.txt clean apkIntelRelease -``` +packer-ng generate --channels=ch1,ch2,ch3 --output=build/archives app.apk +``` -### 命令行打包说明 +* 指定渠道列表文件打包: -**特别提示:如果你同时使用其它的资源压缩工具或应用加固功能,请使用命令行脚本打包增加渠道信息,增加渠道信息需要放在APK处理过程的最后一步。** +```shell +packer-ng generate --channels=@channels.txt --output=build/archives app.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 -usage: PackerNg-1.0.9.py [-h] [-f [FORMAT]] [-s] [-t TEST] - [apkfile] [marketfile] [output] - -positional arguments: - apkfile original release apk file path (required) - marketfile markets file path [default: ./markets.txt] - output archives output path [default: ./archives] - -optional arguments: - -h, --help show this help message and exit - -f [FORMAT], --format [FORMAT] - archive format [default:'${name}-${package}-v${vname}- - ${vcode}-${market}${ext}'] - -s, --show show apk file info (pkg/market/version) - -t TEST, --test TEST perform serval times packer-ng test +python tools/packer-ng-v2.py app.apk ``` -#### 不使用Gradle -使用命令行打包脚本,不想添加Gradle依赖的,可以完全忽略Gradle的配置,直接复制 [PackerNg.java](helper/src/main/java/com/mcxiaoke/packer/helper/PackerNg.java) 到项目中使用即可。 +* C程序读取渠道: -### 插件配置说明(可选) +```shell +cd tools +make +make install +packer app.apk +``` -```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 channel = PackerNg.getChannel(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) +## 其它说明 -### 同类工具 +渠道读取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) ,特此致谢。 -- [**gradle-packer-plugin**](https://github.com/mcxiaoke/gradle-packer-plugin) - 旧版渠道打包工具,完全使用Gradle系统实现,能利用Android提供的productFlavors系统的条件编译功能,无任何兼容性问题,方便集成,但是由于每次都要重新打包,速度比较慢,不适合需要大量打包的情况。(性能:200个渠道包需要一到两小时) ------ @@ -244,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) #### 开源项目 @@ -263,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. diff --git a/sample/build.gradle b/sample/build.gradle deleted file mode 100644 index d6b0547..0000000 --- a/sample/build.gradle +++ /dev/null @@ -1,147 +0,0 @@ -buildscript { - ext.packer_version = '1.0.9-SNAPSHOT' - - repositories { - maven { url '/tmp/repo/' } - jcenter() - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } - } - - dependencies { - classpath "com.android.tools.build:gradle:2.2.1" - classpath "com.mcxiaoke.gradle:packer-ng:$packer_version" - } -} - -repositories { - maven { url '/tmp/repo/' } - jcenter() - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } -} - -apply plugin: 'com.android.application' -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 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.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') { - exclude group: 'com.android.support', module: 'support-v4' - } -} - -packer { - checkSigningConfig = true - checkZipAlign = true - archiveNameFormat = '${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}-${fileMD5}' - archiveOutput = file(new File(project.rootProject.buildDir.path, "myapks")) -} - -android { - 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) - minSdkVersion Integer.parseInt(project.MIN_SDK_VERSION) - targetSdkVersion Integer.parseInt(project.TARGET_SDK_VERSION) - } - - signingConfigs { - release { - storeFile file("android.keystore") - storePassword "android" - keyAlias "android" - keyPassword "android" - v2SigningEnabled false - } - } - - buildTypes { - release { - signingConfig signingConfigs.release - minifyEnabled false - } - - //someType { - // minifyEnabled false - // debuggable true - //} - beta { - signingConfig signingConfigs.release - minifyEnabled false - debuggable true - } - - nosign { - // invalid: no signingConfig - minifyEnabled false - } - - - } - - productFlavors { - cat { - } - - dog { - signingConfig signingConfigs.release - } - } - - lintOptions { - abortOnError false - htmlReport true - } - - packagingOptions { - exclude 'LICENSE.txt' - } - -} - -//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 -//} diff --git a/sample/gradle.properties b/sample/gradle.properties deleted file mode 100644 index 1a013dc..0000000 --- a/sample/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -#market=markets.txt 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/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java b/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java deleted file mode 100644 index 1ec9760..0000000 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/MainActivity.java +++ /dev/null @@ -1,252 +0,0 @@ -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.os.Bundle; -import android.support.v7.app.ActionBarActivity; -import android.util.DisplayMetrics; -import android.view.Display; -import android.view.ViewGroup; -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.ng.sample.BuildConfig; -import com.mcxiaoke.packer.ng.sample.R; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Set; - - -public class MainActivity extends ActionBarActivity { - 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("Market: ").append(PackerNg.getMarket(this)).append("\n"); - builder.append("MarketInfo: ").append(PackerNg.getMarketInfo(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) { - } - - - } - - 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(); - } - -} 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 7423ebc..0000000 --- a/sample/src/main/java/com/mcxiaoke/packer/samples/ResUtils.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.mcxiaoke.packer.samples; - -import android.content.Context; -import com.mcxiaoke.packer.helper.PackerNg; - -/** - * User: mcxiaoke - * Date: 16/1/13 - * Time: 15:11 - */ -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); - } - - 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 236bff5..0000000 Binary files a/sample/src/main/res/drawable-hdpi/drawer_shadow.9.png and /dev/null differ diff --git a/sample/src/main/res/drawable-hdpi/ic_drawer.png b/sample/src/main/res/drawable-hdpi/ic_drawer.png deleted file mode 100644 index c59f601..0000000 Binary files a/sample/src/main/res/drawable-hdpi/ic_drawer.png and /dev/null differ diff --git a/sample/src/main/res/drawable-hdpi/ic_launcher.png b/sample/src/main/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index 96a442e..0000000 Binary files a/sample/src/main/res/drawable-hdpi/ic_launcher.png and /dev/null differ diff --git a/sample/src/main/res/drawable-mdpi/drawer_shadow.9.png b/sample/src/main/res/drawable-mdpi/drawer_shadow.9.png deleted file mode 100644 index ffe3a28..0000000 Binary files a/sample/src/main/res/drawable-mdpi/drawer_shadow.9.png and /dev/null differ diff --git a/sample/src/main/res/drawable-mdpi/ic_drawer.png b/sample/src/main/res/drawable-mdpi/ic_drawer.png deleted file mode 100644 index 1ed2c56..0000000 Binary files a/sample/src/main/res/drawable-mdpi/ic_drawer.png and /dev/null differ diff --git a/sample/src/main/res/drawable-mdpi/ic_launcher.png b/sample/src/main/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index 359047d..0000000 Binary files a/sample/src/main/res/drawable-mdpi/ic_launcher.png and /dev/null differ 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 fabe9d9..0000000 Binary files a/sample/src/main/res/drawable-xhdpi/drawer_shadow.9.png and /dev/null differ 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 a5fa74d..0000000 Binary files a/sample/src/main/res/drawable-xhdpi/ic_drawer.png and /dev/null differ diff --git a/sample/src/main/res/drawable-xhdpi/ic_launcher.png b/sample/src/main/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index 71c6d76..0000000 Binary files a/sample/src/main/res/drawable-xhdpi/ic_launcher.png and /dev/null differ 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 b91e9d7..0000000 Binary files a/sample/src/main/res/drawable-xxhdpi/drawer_shadow.9.png and /dev/null differ 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 9c4685d..0000000 Binary files a/sample/src/main/res/drawable-xxhdpi/ic_drawer.png and /dev/null differ 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 @@ - - - - - - diff --git a/settings.gradle b/settings.gradle index be487dc..d515779 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,5 @@ +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!" diff --git a/tools/PackerNg-1.0.9.jar b/tools/PackerNg-1.0.9.jar deleted file mode 100644 index d06a18b..0000000 Binary files a/tools/PackerNg-1.0.9.jar and /dev/null differ 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/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()} - } -} 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/packer-ng-2.0.0.jar b/tools/packer-ng-2.0.0.jar new file mode 100644 index 0000000..15d3108 Binary files /dev/null and b/tools/packer-ng-2.0.0.jar differ diff --git a/tools/packer-ng-v2.py b/tools/packer-ng-v2.py new file mode 100644 index 0000000..d74189b --- /dev/null +++ b/tools/packer-ng-v2.py @@ -0,0 +1,548 @@ +# -*- coding: utf-8 -*- +# @Author: mcxiaoke +# @Date: 2017-06-06 14:03:18 +# @Last Modified by: mcxiaoke +# @Last Modified time: 2017-06-22 17:26:29 +from __future__ import print_function +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) +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()) + VERSION = props.get('VERSION_NAME') +except Exception as e: + VERSION = '2.0.0' + +##################################################################### + + +# 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 + +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 +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): + '''ZipFormatException''' + pass + + +class SignatureNotFoundException(Exception): + '''SignatureNotFoundException''' + pass + + +class MagicNotFoundException(Exception): + '''MagicNotFoundException''' + 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 + +##################################################################### + + +def parseValues(content): + ''' + 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 + @-4 payload length same as @+16 4 bytes + ''' + magicLen = len(PLUGIN_BLOCK_MAGIC) + logger.debug('content:%s', content) + if not content or len(content) < magicLen + 4 * 2: + return None + 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 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=length, + offset=offset, + access=mmap.ACCESS_READ) + + +def findBlockByPluginMagic(apk): + 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 findBlockBySigningMagic(apk): + # search APK Signing Block Magic words + signingBlock = findBySigningMagic(apk) + if signingBlock: + return parseApkSigningBlock(signingBlock, PLUGIN_BLOCK_ID) + + +def findBlockByZipSections(apk): + # find zip centralDirectory, then find apkSigningBlock + signingBlock = findByZipSections(apk) + if signingBlock: + return parseApkSigningBlock(signingBlock, PLUGIN_BLOCK_ID) + + +def findBySigningMagic(apk): + # findApkSigningBlockUsingSigningMagic + 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): + # findApkSigningBlockUsingZipSections + with open(apk, "rb") as f: + mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) + sections = findZipSections(mm) + + centralDirStartOffset = sections.cdStartOffset + centralDirEndOffset = centralDirStartOffset + sections.cdSizeBytes + eocdStartOffset = sections.eocdOffset + logger.debug('centralDirStartOffset:%s', centralDirStartOffset) + logger.debug('centralDirEndOffset:%s', centralDirEndOffset) + logger.debug('eocdStartOffset:%s', eocdStartOffset) + if centralDirEndOffset != eocdStartOffset: + raise SignatureNotFoundException( + "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:{} " + .format(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 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) + + if apkSigBlockSizeInFooter < footerSize or \ + apkSigBlockSizeInFooter > sys.maxsize - 8: + raise SignatureNotFoundException( + "APK Signing Block size out of range: {}" + .format(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: {} vs {}" + .format(apkSigBlockSizeInHeader, apkSigBlockSizeInFooter)) + + 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: + // 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 + ''' + 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('payloadSize:%s', size) + + entryCount = 0 + position = 0 + signingBlock = None + channelBlock = None + while position < size: + entryCount += 1 + 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.maxsize - 8: + raise SignatureNotFoundException( + "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 #{}, available: {}" + .format(entryCount, (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: + 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: {}" + ". 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 + logger.debug('cdEndOffset:%s', cdEndOffset) + if cdEndOffset > eocdOffset: + raise ZipFormatException( + "ZIP Central Directory overlaps with End of Central Directory" + ". CD end: {}, EoCD start: {}" + .format(cdEndOffset, 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) + eocdEmptyCommentStartPos = archiveSize - ZIP_EOCD_REC_MIN_SIZE + logger.debug('eocdEmptyCommentStartPos:%s', + eocdEmptyCommentStartPos) + expectedCommentLength = 0 + eocdOffsetInBuf = -1 + while expectedCommentLength <= maxCommentLength: + eocdStartPos = eocdEmptyCommentStartPos - expectedCommentLength + logger.debug('expectedCommentLength:%s', expectedCommentLength) + # logger.debug('eocdStartPos:%s', eocdStartPos) + 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 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 + + +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', apk) + try: + zp = zipfile.ZipFile(apk) + zp.testzip() + content = findBlockByZipSections(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) + + +def showInfo(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 + + +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]) + channel = getChannel(apk) + print('Channel: \t{}'.format(channel)) + showInfo(apk) + + +if __name__ == '__main__': + main() 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/src/Makefile b/tools/src/Makefile new file mode 100644 index 0000000..5a3075d --- /dev/null +++ b/tools/src/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) diff --git a/tools/src/read.c b/tools/src/read.c new file mode 100644 index 0000000..b98b944 --- /dev/null +++ b/tools/src/read.c @@ -0,0 +1,194 @@ +/* + * @Author: mcxiaoke + * @Date: 2017-06-13 15:47:02 + * @Last Modified by: mcxiaoke + * @Last Modified time: 2017-06-13 18:23:41 + */ +//#include "config.h" +#include +#include +#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 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 { \ + printf(msg); \ + exit(EXIT_FAILURE); \ + } while (0) + +#define handle_not_found() \ + do { \ + printf("Channel not found\n"); \ + exit(EXIT_FAILURE); \ + } while (0) + +/* 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; +} + +/* +* finds the position of the pattern in the given target string +* target - str, patter - word +*/ +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); + 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; +} + +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; + struct stat sb; + off_t offset, pa_offset; + 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); + } + 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 { + 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("Can not mmap\n"); + } + + 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); + char payload[payload_len + 1]; + strncpy_2(payload, &addr[li + 4], payload_len); + // payload[payload_len] = '\0'; + // printf("payload=%s\n", payload); + 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 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