diff --git a/apktool-cli/src/main/java/brut/apktool/Main.java b/apktool-cli/src/main/java/brut/apktool/Main.java index bb5737ba..a02e985c 100644 --- a/apktool-cli/src/main/java/brut/apktool/Main.java +++ b/apktool-cli/src/main/java/brut/apktool/Main.java @@ -16,7 +16,9 @@ package brut.apktool; -import brut.androlib.*; +import brut.androlib.Androlib; +import brut.androlib.AndrolibException; +import brut.androlib.ApkDecoder; import brut.androlib.err.CantFindFrameworkResException; import brut.androlib.err.InFileNotFoundException; import brut.androlib.err.OutDirExistsException; @@ -209,6 +211,7 @@ public class Main { private static void usage() { System.out.println( "Apktool v" + Androlib.getVersion() + " - a tool for reengineering Android apk files\n" + + "Edited by iBotPeaches (@iBotPeaches) \n" + "Copyright 2010 Ryszard Wiśniewski \n" + "Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)\n" + "\n" + @@ -236,7 +239,7 @@ public class Main { " \"Invalid config flags detected. Dropping resources\", but you\n" + " want to decode them anyway, even with errors. You will have to\n" + " fix them manually before building." + - "\n" + + "\n\n" + " b[uild] [OPTS] [] []\n" + " Build an apk from already decoded application located in .\n" + "\n" + diff --git a/apktool-cli/src/main/java/brut/apktool/Main.java~ b/apktool-cli/src/main/java/brut/apktool/Main.java~ new file mode 100644 index 00000000..d38d1f28 --- /dev/null +++ b/apktool-cli/src/main/java/brut/apktool/Main.java~ @@ -0,0 +1,299 @@ +/** + * Copyright 2011 Ryszard Wiśniewski + * + * 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 brut.apktool; + +import brut.androlib.*; +import brut.androlib.err.CantFindFrameworkResException; +import brut.androlib.err.InFileNotFoundException; +import brut.androlib.err.OutDirExistsException; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.logging.*; + +/** + * @author Ryszard Wiśniewski + */ +public class Main { + public static void main(String[] args) + throws IOException, AndrolibException, InterruptedException { + try { + Verbosity verbosity = Verbosity.NORMAL; + int i; + for (i = 0; i < args.length; i++) { + String opt = args[i]; + if (! opt.startsWith("-")) { + break; + } + if ("-v".equals(opt) || "--verbose".equals(opt)) { + if (verbosity != Verbosity.NORMAL) { + throw new InvalidArgsError(); + } + verbosity = Verbosity.VERBOSE; + } else if ("-q".equals(opt) || "--quiet".equals(opt)) { + if (verbosity != Verbosity.NORMAL) { + throw new InvalidArgsError(); + } + verbosity = Verbosity.QUIET; + } else { + throw new InvalidArgsError(); + } + } + setupLogging(verbosity); + + if (args.length <= i) { + throw new InvalidArgsError(); + } + String cmd = args[i]; + args = Arrays.copyOfRange(args, i + 1, args.length); + + if ("d".equals(cmd) || "decode".equals(cmd)) { + cmdDecode(args); + } else if ("b".equals(cmd) || "build".equals(cmd)) { + cmdBuild(args); + } else if ("if".equals(cmd) || "install-framework".equals(cmd)) { + cmdInstallFramework(args); + } else if ("publicize-resources".equals(cmd)) { + cmdPublicizeResources(args); + } else { + throw new InvalidArgsError(); + } + } catch (InvalidArgsError ex) { + usage(); + System.exit(1); + } + } + + private static void cmdDecode(String[] args) throws InvalidArgsError, + AndrolibException { + ApkDecoder decoder = new ApkDecoder(); + + int i; + for (i = 0; i < args.length; i++) { + String opt = args[i]; + if (! opt.startsWith("-")) { + break; + } + if ("-s".equals(opt) || "--no-src".equals(opt)) { + decoder.setDecodeSources(ApkDecoder.DECODE_SOURCES_NONE); + } else if ("-d".equals(opt) || "--debug".equals(opt)) { + decoder.setDebugMode(true); + } else if ("-t".equals(opt) || "--frame-tag".equals(opt)) { + i++; + if (i >= args.length) { + throw new InvalidArgsError(); + } + decoder.setFrameworkTag(args[i]); + } else if ("-f".equals(opt) || "--force".equals(opt)) { + decoder.setForceDelete(true); + } else if ("-r".equals(opt) || "--no-res".equals(opt)) { + decoder.setDecodeResources(ApkDecoder.DECODE_RESOURCES_NONE); + } else if ("--keep-broken-res".equals(opt)) { + decoder.setKeepBrokenResources(true); + } else { + throw new InvalidArgsError(); + } + } + + String outName = null; + if (args.length == i + 2) { + outName = args[i + 1]; + } else if (args.length == i + 1) { + outName = args[i]; + outName = outName.endsWith(".apk") ? + outName.substring(0, outName.length() - 4) : outName + ".out"; + outName = new File(outName).getName(); + } else { + throw new InvalidArgsError(); + } + File outDir = new File(outName); + decoder.setOutDir(outDir); + decoder.setApkFile(new File(args[i])); + + try { + decoder.decode(); + } catch (OutDirExistsException ex) { + System.out.println( + "Destination directory (" + outDir.getAbsolutePath() + ") " + + "already exists. Use -f switch if you want to overwrite it."); + System.exit(1); + } catch (InFileNotFoundException ex) { + System.out.println( + "Input file (" + args[i] + ") " + + "was not found or was not readable."); + System.exit(1); + } catch (CantFindFrameworkResException ex) { + System.out.println( + "Can't find framework resources for package of id: " + + String.valueOf(ex.getPkgId()) + ". You must install proper " + + "framework files, see project website for more info."); + System.exit(1); + } + } + + private static void cmdBuild(String[] args) throws InvalidArgsError, + AndrolibException { + boolean forceBuildAll = false; + boolean debug = false; + int i; + for (i = 0; i < args.length; i++) { + String opt = args[i]; + if (! opt.startsWith("-")) { + break; + } + if ("-f".equals(opt) || "--force-all".equals(opt)) { + forceBuildAll = true; + } else if ("-d".equals(opt) || "--debug".equals(opt)) { + debug = true; + } else { + throw new InvalidArgsError(); + } + } + + String appDirName; + File outFile = null; + switch (args.length - i) { + case 0: + appDirName = "."; + break; + case 2: + outFile = new File(args[i + 1]); + case 1: + appDirName = args[i]; + break; + default: + throw new InvalidArgsError(); + } + + new Androlib().build(new File(appDirName), outFile, forceBuildAll, + debug); + } + + private static void cmdInstallFramework(String[] args) + throws AndrolibException { + String tag = null; + switch (args.length) { + case 2: + tag = args[1]; + case 1: + new Androlib().installFramework(new File(args[0]), tag); + return; + } + + throw new InvalidArgsError(); + } + + private static void cmdPublicizeResources(String[] args) + throws InvalidArgsError, AndrolibException { + if (args.length != 1) { + throw new InvalidArgsError(); + } + + new Androlib().publicizeResources(new File(args[0])); + } + + private static void usage() { + System.out.println( + "Apktool v" + Androlib.getVersion() + " - a tool for reengineering Android apk files\n" + + "Edited by iBotPeachs of http://miuiandroid.com\n" + + "Copyright 2010 Ryszard Wiśniewski \n" + + "Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)\n" + + "\n" + + "Usage: apktool [-q|--quiet OR -v|--verbose] COMMAND [...]\n" + + "\n" + + "COMMANDs are:\n" + + "\n" + + " d[ecode] [OPTS] []\n" + + " Decode to .\n" + + "\n" + + " OPTS:\n" + + "\n" + + " -s, --no-src\n" + + " Do not decode sources.\n" + + " -r, --no-res\n" + + " Do not decode resources.\n" + + " -d, --debug\n" + + " Decode in debug mode. Check project page for more info.\n" + + " -f, --force\n" + + " Force delete destination directory.\n" + + " -t , --frame-tag \n" + + " Try to use framework files tagged by .\n" + + " --keep-broken-res\n" + + " Use if there was an error and some resources were dropped, e.g.:\n" + + " \"Invalid config flags detected. Dropping resources\", but you\n" + + " want to decode them anyway, even with errors. You will have to\n" + + " fix them manually before building." + + "\n" + + " b[uild] [OPTS] [] []\n" + + " Build an apk from already decoded application located in .\n" + + "\n" + + " It will automatically detect, whether files was changed and perform\n" + + " needed steps only.\n" + + "\n" + + " If you omit then current directory will be used.\n" + + " If you omit then /dist/\n" + + " will be used.\n" + + "\n" + + " OPTS:\n" + + "\n" + + " -f, --force-all\n" + + " Skip changes detection and build all files.\n" + + " -d, --debug\n" + + " Build in debug mode. Check project page for more info.\n" + + "\n" + + " if|install-framework []\n" + + " Install framework file to your system.\n" + + "\n" + + "For additional info, see: http://code.google.com/p/android-apktool/" + ); + } + + private static void setupLogging(Verbosity verbosity) { + Logger logger = Logger.getLogger(""); + for (Handler handler : logger.getHandlers()) { + logger.removeHandler(handler); + } + if (verbosity == Verbosity.QUIET) { + return; + } + + Handler handler = new ConsoleHandler(); + logger.addHandler(handler); + + if (verbosity == Verbosity.VERBOSE) { + handler.setLevel(Level.ALL); + logger.setLevel(Level.ALL); + } else { + handler.setFormatter(new Formatter() { + @Override + public String format(LogRecord record) { + return record.getLevel().toString().charAt(0) + ": " + + record.getMessage() + + System.getProperty("line.separator"); + } + }); + } + } + + private static enum Verbosity { + NORMAL, VERBOSE, QUIET; + } + + static class InvalidArgsError extends AndrolibException { + + } +} diff --git a/apktool-lib/pom.xml b/apktool-lib/pom.xml index da8d6a45..d0e262fd 100644 --- a/apktool-lib/pom.xml +++ b/apktool-lib/pom.xml @@ -4,7 +4,7 @@ brut.apktool apktool-lib - 1.4.4-SNAPSHOT + 1.4.5-SNAPSHOT jar diff --git a/apktool-lib/src/main/java/brut/androlib/Androlib.java b/apktool-lib/src/main/java/brut/androlib/Androlib.java index 24a64761..9124ee78 100644 --- a/apktool-lib/src/main/java/brut/androlib/Androlib.java +++ b/apktool-lib/src/main/java/brut/androlib/Androlib.java @@ -24,7 +24,8 @@ import brut.androlib.res.util.ExtFile; import brut.androlib.src.SmaliBuilder; import brut.androlib.src.SmaliDecoder; import brut.common.BrutException; -import brut.directory.*; +import brut.directory.Directory; +import brut.directory.DirectoryException; import brut.util.BrutIO; import brut.util.OS; import java.io.*; diff --git a/apktool-lib/src/main/java/brut/androlib/Androlib.java~ b/apktool-lib/src/main/java/brut/androlib/Androlib.java~ new file mode 100644 index 00000000..fb3522ab --- /dev/null +++ b/apktool-lib/src/main/java/brut/androlib/Androlib.java~ @@ -0,0 +1,443 @@ +/** + * Copyright 2011 Ryszard Wiśniewski + * + * 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 brut.androlib; + +import brut.androlib.java.AndrolibJava; +import brut.androlib.res.AndrolibResources; +import brut.androlib.res.data.ResPackage; +import brut.androlib.res.data.ResTable; +import brut.androlib.res.util.ExtFile; +import brut.androlib.src.SmaliBuilder; +import brut.androlib.src.SmaliDecoder; +import brut.common.BrutException; +import brut.directory.*; +import brut.util.BrutIO; +import brut.util.OS; +import java.io.*; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +/** + * @author Ryszard Wiśniewski + */ +public class Androlib { + private final AndrolibResources mAndRes = new AndrolibResources(); + + public ResTable getResTable(ExtFile apkFile) throws AndrolibException { + return mAndRes.getResTable(apkFile); + } + + public void decodeSourcesRaw(ExtFile apkFile, File outDir, boolean debug) + throws AndrolibException { + try { + if (debug) { + LOGGER.warning("Debug mode not available."); + } + Directory apk = apkFile.getDirectory(); + LOGGER.info("Copying raw classes.dex file..."); + apkFile.getDirectory().copyToDir(outDir, "classes.dex"); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } + } + + public void decodeSourcesSmali(File apkFile, File outDir, boolean debug) + throws AndrolibException { + try { + File smaliDir = new File(outDir, SMALI_DIRNAME); + OS.rmdir(smaliDir); + smaliDir.mkdirs(); + LOGGER.info("Baksmaling..."); + SmaliDecoder.decode(apkFile, smaliDir, debug); + } catch (BrutException ex) { + throw new AndrolibException(ex); + } + } + + public void decodeSourcesJava(ExtFile apkFile, File outDir, boolean debug) + throws AndrolibException { + LOGGER.info("Decoding Java sources..."); + new AndrolibJava().decode(apkFile, outDir); + } + + public void decodeResourcesRaw(ExtFile apkFile, File outDir) + throws AndrolibException { + try { + Directory apk = apkFile.getDirectory(); + LOGGER.info("Copying raw resources..."); + apkFile.getDirectory().copyToDir(outDir, APK_RESOURCES_FILENAMES); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } + } + + public void decodeResourcesFull(ExtFile apkFile, File outDir, + ResTable resTable) throws AndrolibException { + mAndRes.decode(resTable, apkFile, outDir); + } + + public void decodeRawFiles(ExtFile apkFile, File outDir) + throws AndrolibException { + LOGGER.info("Copying assets and libs..."); + try { + Directory in = apkFile.getDirectory(); + if (in.containsDir("assets")) { + in.copyToDir(outDir, "assets"); + } + if (in.containsDir("lib")) { + in.copyToDir(outDir, "lib"); + } + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } + } + + public void writeMetaFile(File mOutDir, Map meta) + throws AndrolibException { + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); +// options.setIndent(4); + Yaml yaml = new Yaml(options); + + FileWriter writer = null; + try { + writer = new FileWriter(new File(mOutDir, "apktool.yml")); + yaml.dump(meta, writer); + } catch (IOException ex) { + throw new AndrolibException(ex); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ex) {} + } + } + } + + public Map readMetaFile(ExtFile appDir) + throws AndrolibException { + InputStream in = null; + try { + in = appDir.getDirectory().getFileInput("apktool.yml"); + Yaml yaml = new Yaml(); + return (Map) yaml.load(in); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ex) {} + } + } + } + + public void build(File appDir, File outFile, boolean forceBuildAll, + boolean debug) throws AndrolibException { + build(new ExtFile(appDir), outFile, forceBuildAll, debug); + } + + public void build(ExtFile appDir, File outFile, boolean forceBuildAll, + boolean debug) throws AndrolibException { + Map meta = readMetaFile(appDir); + Object t1 = meta.get("isFrameworkApk"); + boolean framework = t1 == null ? false : (Boolean) t1; + + if (outFile == null) { + String outFileName = (String) meta.get("apkFileName"); + outFile = new File(appDir, "dist" + File.separator + + (outFileName == null ? "out.apk" : outFileName)); + } + + new File(appDir, APK_DIRNAME).mkdirs(); + buildSources(appDir, forceBuildAll, debug); + buildResources(appDir, forceBuildAll, framework, + (Map) meta.get("usesFramework")); + buildLib(appDir, forceBuildAll); + buildApk(appDir, outFile, framework); + } + + public void buildSources(File appDir, boolean forceBuildAll, boolean debug) + throws AndrolibException { + if (! buildSourcesRaw(appDir, forceBuildAll, debug) + && ! buildSourcesSmali(appDir, forceBuildAll, debug) + && ! buildSourcesJava(appDir, forceBuildAll, debug) + ) { + LOGGER.warning("Could not find sources"); + } + } + + public boolean buildSourcesRaw(File appDir, boolean forceBuildAll, + boolean debug) throws AndrolibException { + try { + File working = new File(appDir, "classes.dex"); + if (! working.exists()) { + return false; + } + if (debug) { + LOGGER.warning("Debug mode not available."); + } + File stored = new File(appDir, APK_DIRNAME + "/classes.dex"); + if (forceBuildAll || isModified(working, stored)) { + LOGGER.info("Copying classes.dex file..."); + BrutIO.copyAndClose(new FileInputStream(working), + new FileOutputStream(stored)); + } + return true; + } catch (IOException ex) { + throw new AndrolibException(ex); + } + } + + public boolean buildSourcesSmali(File appDir, boolean forceBuildAll, + boolean debug) throws AndrolibException { + ExtFile smaliDir = new ExtFile(appDir, "smali"); + if (! smaliDir.exists()) { + return false; + } + File dex = new File(appDir, APK_DIRNAME + "/classes.dex"); + if (! forceBuildAll) { + LOGGER.info("Checking whether sources has changed..."); + } + if (forceBuildAll || isModified(smaliDir, dex)) { + LOGGER.info("Smaling..."); + dex.delete(); + SmaliBuilder.build(smaliDir, dex, debug); + } + return true; + } + + public boolean buildSourcesJava(File appDir, boolean forceBuildAll, + boolean debug) throws AndrolibException { + File javaDir = new File(appDir, "src"); + if (! javaDir.exists()) { + return false; + } + File dex = new File(appDir, APK_DIRNAME + "/classes.dex"); + if (! forceBuildAll) { + LOGGER.info("Checking whether sources has changed..."); + } + if (forceBuildAll || isModified(javaDir, dex)) { + LOGGER.info("Building java sources..."); + dex.delete(); + new AndrolibJava().build(javaDir, dex); + } + return true; + } + + public void buildResources(ExtFile appDir, boolean forceBuildAll, + boolean framework, Map usesFramework) + throws AndrolibException { + if (! buildResourcesRaw(appDir, forceBuildAll) + && ! buildResourcesFull(appDir, forceBuildAll, framework, + usesFramework)) { + LOGGER.warning("Could not find resources"); + } + } + + public boolean buildResourcesRaw(ExtFile appDir, boolean forceBuildAll) + throws AndrolibException { + try { + if (! new File(appDir, "resources.arsc").exists()) { + return false; + } + File apkDir = new File(appDir, APK_DIRNAME); + if (! forceBuildAll) { + LOGGER.info("Checking whether resources has changed..."); + } + if (forceBuildAll || isModified( + newFiles(APK_RESOURCES_FILENAMES, appDir), + newFiles(APK_RESOURCES_FILENAMES, apkDir))) { + LOGGER.info("Copying raw resources..."); + appDir.getDirectory() + .copyToDir(apkDir, APK_RESOURCES_FILENAMES); + } + return true; + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } + } + + public boolean buildResourcesFull(File appDir, boolean forceBuildAll, + boolean framework, Map usesFramework) + throws AndrolibException { + try { + if (! new File(appDir, "res").exists()) { + return false; + } + if (! forceBuildAll) { + LOGGER.info("Checking whether resources has changed..."); + } + File apkDir = new File(appDir, APK_DIRNAME); + if (forceBuildAll || isModified( + newFiles(APP_RESOURCES_FILENAMES, appDir), + newFiles(APK_RESOURCES_FILENAMES, apkDir))) { + LOGGER.info("Building resources..."); + + File apkFile = File.createTempFile("APKTOOL", null); + apkFile.delete(); + + File ninePatch = new File(appDir, "9patch"); + if (! ninePatch.exists()) { + ninePatch = null; + } + mAndRes.aaptPackage( + apkFile, + new File(appDir, "AndroidManifest.xml"), + new File(appDir, "res"), + ninePatch, null, parseUsesFramework(usesFramework), + false, framework + ); + + Directory tmpDir = new ExtFile(apkFile).getDirectory(); + tmpDir.copyToDir(apkDir, + tmpDir.containsDir("res") ? APK_RESOURCES_FILENAMES : + APK_RESOURCES_WITHOUT_RES_FILENAMES); + } + return true; + } catch (IOException ex) { + throw new AndrolibException(ex); + } catch (DirectoryException ex) { + //throw new AndrolibException(ex); + } + } + + public void buildLib(File appDir, boolean forceBuildAll) + throws AndrolibException { + File working = new File(appDir, "lib"); + if (! working.exists()) { + return; + } + File stored = new File(appDir, APK_DIRNAME + "/lib"); + if (forceBuildAll || isModified(working, stored)) { + LOGGER.info("Copying libs..."); + try { + OS.rmdir(stored); + OS.cpdir(working, stored); + } catch (BrutException ex) { + throw new AndrolibException(ex); + } + } + } + + public void buildApk(File appDir, File outApk, boolean framework) + throws AndrolibException { + LOGGER.info("Building apk file..."); + if (outApk.exists()) { + outApk.delete(); + } else { + File outDir = outApk.getParentFile(); + if (outDir != null && ! outDir.exists()) { + outDir.mkdirs(); + } + } + File assetDir = new File(appDir, "assets"); + if (! assetDir.exists()) { + assetDir = null; + } + mAndRes.aaptPackage(outApk, null, null, + new File(appDir, APK_DIRNAME), assetDir, null, false, framework); + } + + public void publicizeResources(File arscFile) throws AndrolibException { + mAndRes.publicizeResources(arscFile); + } + + public void installFramework(File frameFile, String tag) + throws AndrolibException { + mAndRes.installFramework(frameFile, tag); + } + + public boolean isFrameworkApk(ResTable resTable) { + for (ResPackage pkg : resTable.listMainPackages()) { + if (pkg.getId() < 64) { + return true; + } + } + return false; + } + + public static String getVersion() { + String version = ApktoolProperties.get("version"); + return version.endsWith("-SNAPSHOT") ? + version.substring(0, version.length() - 9) + '.' + + ApktoolProperties.get("git.commit.id.abbrev") + : version; + } + + private File[] parseUsesFramework(Map usesFramework) + throws AndrolibException { + if (usesFramework == null) { + return null; + } + + List ids = (List) usesFramework.get("ids"); + if (ids == null || ids.isEmpty()) { + return null; + } + + String tag = (String) usesFramework.get("tag"); + File[] files = new File[ids.size()]; + int i = 0; + for (int id : ids) { + files[i++] = mAndRes.getFrameworkApk(id, tag); + } + + return files; + } + + private boolean isModified(File working, File stored) { + if (! stored.exists()) { + return true; + } + return BrutIO.recursiveModifiedTime(working) > + BrutIO.recursiveModifiedTime(stored); + } + + private boolean isModified(File[] working, File[] stored) { + for (int i = 0; i < stored.length; i++) { + if (! stored[i].exists()) { + return true; + } + } + return BrutIO.recursiveModifiedTime(working) > + BrutIO.recursiveModifiedTime(stored); + } + + private File[] newFiles(String[] names, File dir) { + File[] files = new File[names.length]; + for (int i = 0; i < names.length; i++) { + files[i] = new File(dir, names[i]); + } + return files; + } + + private final static Logger LOGGER = + Logger.getLogger(Androlib.class.getName()); + + private final static String SMALI_DIRNAME = "smali"; + private final static String APK_DIRNAME = "build/apk"; + private final static String[] APK_RESOURCES_FILENAMES = + new String[]{"resources.arsc", "AndroidManifest.xml", "res"}; + private final static String[] APK_RESOURCES_WITHOUT_RES_FILENAMES = + new String[]{"resources.arsc", "AndroidManifest.xml"}; + private final static String[] APP_RESOURCES_FILENAMES = + new String[]{"AndroidManifest.xml", "res"}; +} diff --git a/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java b/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java index 3186a798..a39128b8 100644 --- a/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java +++ b/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java @@ -26,7 +26,10 @@ import brut.common.BrutException; import brut.directory.DirectoryException; import brut.util.OS; import java.io.File; -import java.util.*; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; /** * @author Ryszard Wiśniewski diff --git a/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java~ b/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java~ new file mode 100644 index 00000000..3186a798 --- /dev/null +++ b/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java~ @@ -0,0 +1,234 @@ +/** + * Copyright 2011 Ryszard Wiśniewski + * + * 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 brut.androlib; + +import brut.androlib.err.InFileNotFoundException; +import brut.androlib.err.OutDirExistsException; +import brut.androlib.res.AndrolibResources; +import brut.androlib.res.data.ResPackage; +import brut.androlib.res.data.ResTable; +import brut.androlib.res.util.ExtFile; +import brut.common.BrutException; +import brut.directory.DirectoryException; +import brut.util.OS; +import java.io.File; +import java.util.*; + +/** + * @author Ryszard Wiśniewski + */ +public class ApkDecoder { + public ApkDecoder() { + this(new Androlib()); + } + + public ApkDecoder(Androlib androlib) { + mAndrolib = androlib; + } + + public ApkDecoder(File apkFile) { + this(apkFile, new Androlib()); + } + + public ApkDecoder(File apkFile, Androlib androlib) { + mAndrolib = androlib; + setApkFile(apkFile); + } + + public void setApkFile(File apkFile) { + mApkFile = new ExtFile(apkFile); + mResTable = null; + } + + public void setOutDir(File outDir) throws AndrolibException { + mOutDir = outDir; + } + + public void decode() throws AndrolibException { + File outDir = getOutDir(); + + if (! mForceDelete && outDir.exists()) { + throw new OutDirExistsException(); + } + + if (! mApkFile.isFile() || ! mApkFile.canRead() ) { + throw new InFileNotFoundException(); + } + + try { + OS.rmdir(outDir); + } catch (BrutException ex) { + throw new AndrolibException(ex); + } + outDir.mkdirs(); + + if (hasSources()) { + switch (mDecodeSources) { + case DECODE_SOURCES_NONE: + mAndrolib.decodeSourcesRaw(mApkFile, outDir, mDebug); + break; + case DECODE_SOURCES_SMALI: + mAndrolib.decodeSourcesSmali(mApkFile, outDir, mDebug); + break; + case DECODE_SOURCES_JAVA: + mAndrolib.decodeSourcesJava(mApkFile, outDir, mDebug); + break; + } + } + if (hasResources()) { + switch (mDecodeResources) { + case DECODE_RESOURCES_NONE: + mAndrolib.decodeResourcesRaw(mApkFile, outDir); + break; + case DECODE_RESOURCES_FULL: + mAndrolib.decodeResourcesFull(mApkFile, outDir, + getResTable()); + break; + } + } + mAndrolib.decodeRawFiles(mApkFile, outDir); + writeMetaFile(); + } + + public void setDecodeSources(short mode) throws AndrolibException { + if (mode != DECODE_SOURCES_NONE && mode != DECODE_SOURCES_SMALI + && mode != DECODE_SOURCES_JAVA) { + throw new AndrolibException("Invalid decode sources mode: " + mode); + } + mDecodeSources = mode; + } + + public void setDecodeResources(short mode) throws AndrolibException { + if (mode != DECODE_RESOURCES_NONE && mode != DECODE_RESOURCES_FULL) { + throw new AndrolibException("Invalid decode resources mode"); + } + mDecodeResources = mode; + } + + public void setDebugMode(boolean debug) { + mDebug = debug; + } + + public void setForceDelete(boolean forceDelete) { + mForceDelete = forceDelete; + } + + public void setFrameworkTag(String tag) throws AndrolibException { + mFrameTag = tag; + if (mResTable != null) { + getResTable().setFrameTag(tag); + } + } + + public void setKeepBrokenResources(boolean keepBrokenResources) { + mKeepBrokenResources = keepBrokenResources; + } + + public ResTable getResTable() throws AndrolibException { + if (mResTable == null) { + if (! hasResources()) { + throw new AndrolibException( + "Apk doesn't containt resources.arsc file"); + } + AndrolibResources.sKeepBroken = mKeepBrokenResources; + mResTable = mAndrolib.getResTable(mApkFile); + mResTable.setFrameTag(mFrameTag); + } + return mResTable; + } + + public boolean hasSources() throws AndrolibException { + try { + return mApkFile.getDirectory().containsFile("classes.dex"); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } + } + + public boolean hasResources() throws AndrolibException { + try { + return mApkFile.getDirectory().containsFile("resources.arsc"); + } catch (DirectoryException ex) { + throw new AndrolibException(ex); + } + } + + public final static short DECODE_SOURCES_NONE = 0x0000; + public final static short DECODE_SOURCES_SMALI = 0x0001; + public final static short DECODE_SOURCES_JAVA = 0x0002; + + public final static short DECODE_RESOURCES_NONE = 0x0100; + public final static short DECODE_RESOURCES_FULL = 0x0101; + + + private File getOutDir() throws AndrolibException { + if (mOutDir == null) { + throw new AndrolibException("Out dir not set"); + } + return mOutDir; + } + + private void writeMetaFile() throws AndrolibException { + Map meta = new LinkedHashMap(); + meta.put("version", Androlib.getVersion()); + meta.put("apkFileName", mApkFile.getName()); + + if (mDecodeResources != DECODE_RESOURCES_NONE && hasResources()) { + meta.put("isFrameworkApk", + Boolean.valueOf(mAndrolib.isFrameworkApk(getResTable()))); + putUsesFramework(meta); + } + + mAndrolib.writeMetaFile(mOutDir, meta); + } + + private void putUsesFramework(Map meta) + throws AndrolibException { + Set pkgs = getResTable().listFramePackages(); + if (pkgs.isEmpty()) { + return; + } + + Integer[] ids = new Integer[pkgs.size()]; + int i = 0; + for (ResPackage pkg : pkgs) { + ids[i++] = pkg.getId(); + } + Arrays.sort(ids); + + Map uses = new LinkedHashMap(); + uses.put("ids", ids); + + if (mFrameTag != null) { + uses.put("tag", mFrameTag); + } + + meta.put("usesFramework", uses); + } + + private final Androlib mAndrolib; + + private ExtFile mApkFile; + private File mOutDir; + private ResTable mResTable; + private short mDecodeSources = DECODE_SOURCES_SMALI; + private short mDecodeResources = DECODE_RESOURCES_FULL; + private boolean mDebug = false; + private boolean mForceDelete = false; + private String mFrameTag; + private boolean mKeepBrokenResources = false; +} diff --git a/apktool-lib/src/main/java/brut/androlib/mod/SmaliMod.java b/apktool-lib/src/main/java/brut/androlib/mod/SmaliMod.java index 8927026c..bfc6f617 100644 --- a/apktool-lib/src/main/java/brut/androlib/mod/SmaliMod.java +++ b/apktool-lib/src/main/java/brut/androlib/mod/SmaliMod.java @@ -16,7 +16,9 @@ package brut.androlib.mod; -import java.io.*; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import org.antlr.runtime.*; import org.antlr.runtime.tree.CommonTree; import org.antlr.runtime.tree.CommonTreeNodeStream; diff --git a/apktool-lib/src/main/java/brut/androlib/res/ResSmaliUpdater.java~ b/apktool-lib/src/main/java/brut/androlib/res/ResSmaliUpdater.java~ new file mode 100644 index 00000000..828f5d1f --- /dev/null +++ b/apktool-lib/src/main/java/brut/androlib/res/ResSmaliUpdater.java~ @@ -0,0 +1,167 @@ +/** + * Copyright 2011 Ryszard Wiśniewski + * + * 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 brut.androlib.res; + +import brut.androlib.AndrolibException; +import brut.androlib.err.UndefinedResObject; +import brut.androlib.res.data.ResResSpec; +import brut.androlib.res.data.ResTable; +import brut.directory.Directory; +import brut.directory.DirectoryException; +import brut.directory.FileDirectory; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Iterator; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.io.IOUtils; + +/** + * @author Ryszard Wiśniewski + */ +public class ResSmaliUpdater { + public void tagResIDs(ResTable resTable, File smaliDir) + throws AndrolibException { + Directory dir = null; + try { + dir = new FileDirectory(smaliDir); + } catch (DirectoryException ex) { + throw new AndrolibException( + "Could not tag res IDs", ex); + } + for (String fileName : dir.getFiles(true)) { + try { + tagResIdsForFile(resTable, dir, fileName); + } catch (IOException ex) { + throw new AndrolibException( + "Could not tag resIDs for file: " + fileName, ex); + } catch (DirectoryException ex) { + throw new AndrolibException( + "Could not tag resIDs for file: " + fileName, ex); + } catch (AndrolibException ex) { + throw new AndrolibException( + "Could not tag resIDs for file: " + fileName, ex); + } + } + } + + public void updateResIDs(ResTable resTable, File smaliDir) + throws AndrolibException { + try { + Directory dir = new FileDirectory(smaliDir); + for (String fileName : dir.getFiles(true)) { + Iterator it = + IOUtils.readLines(dir.getFileInput(fileName)).iterator(); + PrintWriter out = new PrintWriter(dir.getFileOutput(fileName)); + while (it.hasNext()) { + String line = it.next(); + out.println(line); + Matcher m1 = RES_NAME_PATTERN.matcher(line); + if (! m1.matches()) { + continue; + } + Matcher m2 = RES_ID_PATTERN.matcher(it.next()); + if (! m2.matches()) { + throw new AndrolibException(); + } + int resID = resTable.getPackage(m1.group(1)) + .getType(m1.group(2)).getResSpec(m1.group(3)) + .getId().id; + if (m2.group(1) != null) { + out.println(String.format( + RES_ID_FORMAT_FIELD, m2.group(1), resID)); + } else { + out.println(String.format( + RES_ID_FORMAT_CONST, m2.group(2), resID)); + } + } + out.close(); + } + } catch (IOException ex) { + throw new AndrolibException( + "Could not tag res IDs for: " + smaliDir.getAbsolutePath(), ex); + } catch (DirectoryException ex) { + throw new AndrolibException( + "Could not tag res IDs for: " + smaliDir.getAbsolutePath(), ex); + } + } + + private void tagResIdsForFile(ResTable resTable, Directory dir, + String fileName) throws IOException, DirectoryException, + AndrolibException { + Iterator it = + IOUtils.readLines(dir.getFileInput(fileName)).iterator(); + PrintWriter out = new PrintWriter(dir.getFileOutput(fileName)); + while (it.hasNext()) { + String line = it.next(); + if (RES_NAME_PATTERN.matcher(line).matches()) { + out.println(line); + out.println(it.next()); + continue; + } + Matcher m = RES_ID_PATTERN.matcher(line); + if (m.matches()) { + int resID = parseResID(m.group(3)); + if (resID != -1) { + try { + ResResSpec spec = resTable.getResSpec(resID); + out.println(String.format( + RES_NAME_FORMAT, spec.getFullName())); + } catch (UndefinedResObject ex) { + if (! R_FILE_PATTERN.matcher(fileName).matches()) { + LOGGER.warning(String.format( + "Undefined resource spec in %s: 0x%08x" + , fileName, resID)); + } + } + } + } + out.println(line); + } + out.close(); + } + + private int parseResID(String resIDHex) { + if (resIDHex.endsWith("ff")) { + return -1; + } + int resID = Integer.valueOf(resIDHex, 16); + if (resIDHex.length() == 4) { + resID = resID << 16; + } + return resID; + } + + private final static String RES_ID_FORMAT_FIELD = + ".field %s:I = 0x%08x"; + private final static String RES_ID_FORMAT_CONST = + " const %s, 0x%08x"; + private final static Pattern RES_ID_PATTERN = Pattern.compile( + "^(?:\\.field (.+?):I =| const(?:|/(?:|high)16) ([pv]\\d+?),) 0x(7[a-f]0[1-9a-f](?:|[0-9a-f]{4}))$"); + private final static String RES_NAME_FORMAT = + "# APKTOOL/RES_NAME: %s"; + private final static Pattern RES_NAME_PATTERN = Pattern.compile( + "^# APKTOOL/RES_NAME: (+)$"); + + private final static Pattern R_FILE_PATTERN = Pattern.compile( + ".*R\\$[a-z]+\\.smali$"); + + private final static Logger LOGGER = + Logger.getLogger(ResSmaliUpdater.class.getName()); +} diff --git a/apktool-lib/src/main/java/brut/androlib/res/data/ResPackage.java b/apktool-lib/src/main/java/brut/androlib/res/data/ResPackage.java index ad36792c..025a1519 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/data/ResPackage.java +++ b/apktool-lib/src/main/java/brut/androlib/res/data/ResPackage.java @@ -16,8 +16,8 @@ package brut.androlib.res.data; -import brut.androlib.err.UndefinedResObject; import brut.androlib.AndrolibException; +import brut.androlib.err.UndefinedResObject; import brut.androlib.res.data.value.ResFileValue; import brut.androlib.res.data.value.ResValueFactory; import brut.androlib.res.xml.ResValuesXmlSerializable; diff --git a/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java b/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java index c5d67d52..ada8b751 100644 --- a/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java +++ b/apktool-lib/src/main/java/brut/androlib/res/decoder/ResFileDecoder.java @@ -23,7 +23,9 @@ import brut.androlib.res.data.value.ResBoolValue; import brut.androlib.res.data.value.ResFileValue; import brut.directory.Directory; import brut.directory.DirectoryException; -import java.io.*; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.logging.Level; import java.util.logging.Logger; diff --git a/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java b/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java index bc296cdc..83997b46 100644 --- a/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java +++ b/apktool-lib/src/main/java/brut/androlib/src/SmaliDecoder.java @@ -19,8 +19,7 @@ package brut.androlib.src; import brut.androlib.AndrolibException; import java.io.File; import java.io.IOException; -import org.jf.baksmali.baksmali; -import org.jf.baksmali.main; +import org.jf.baksmali.*; import org.jf.dexlib.DexFile; /** diff --git a/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java b/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java index b86f0599..cb2f49e5 100644 --- a/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java +++ b/apktool-lib/src/test/java/brut/androlib/BuildAndDecodeTest.java @@ -19,11 +19,19 @@ package brut.androlib; import brut.androlib.res.util.ExtFile; import brut.common.BrutException; import brut.util.OS; -import java.io.*; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; import java.util.logging.Logger; -import org.custommonkey.xmlunit.*; -import org.junit.*; -import static org.junit.Assert.*; +import org.custommonkey.xmlunit.DetailedDiff; +import org.custommonkey.xmlunit.Diff; +import org.custommonkey.xmlunit.ElementNameAndAttributeQualifier; +import org.custommonkey.xmlunit.ElementQualifier; +import org.junit.AfterClass; +import static org.junit.Assert.assertTrue; +import org.junit.BeforeClass; +import org.junit.Test; import org.xml.sax.SAXException; @@ -116,9 +124,7 @@ public class BuildAndDecodeTest { @Test public void qualifiersTest() throws BrutException { - compareValuesFiles("values-mcc004-mnc4-en-rUS-sw100dp-w200dp-h300dp" + - "-xlarge-long-land-television-night-xhdpi-finger-keyssoft-12key" + - "-navhidden-dpad/strings.xml"); + compareValuesFiles("values-mcc004/strings.xml"); } private void compareValuesFiles(String path) throws BrutException { diff --git a/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-mcc004-mnc4-en-rUS-sw100dp-w200dp-h300dp-xlarge-long-land-television-night-xhdpi-finger-keyssoft-12key-navhidden-dpad/strings.xml b/apktool-lib/src/test/resources/brut/apktool/testapp/res/values-mcc004/strings.xml similarity index 100% rename from apktool-lib/src/test/resources/brut/apktool/testapp/res/values-mcc004-mnc4-en-rUS-sw100dp-w200dp-h300dp-xlarge-long-land-television-night-xhdpi-finger-keyssoft-12key-navhidden-dpad/strings.xml rename to apktool-lib/src/test/resources/brut/apktool/testapp/res/values-mcc004/strings.xml diff --git a/pom.xml b/pom.xml index bd78948f..f8d95cb2 100644 --- a/pom.xml +++ b/pom.xml @@ -76,18 +76,18 @@ android-apktool.googlecode.com - http://android-apktool.googlecode.com/svn/m2-releases + http://apktool2.googlecode.com/svn/m2-releases android-apktool.googlecode.com - dav:https://android-apktool.googlecode.com/svn/m2-releases + dav:https://apktool2.googlecode.com/svn/m2-releases android-apktool.googlecode.com - dav:https://android-apktool.googlecode.com/svn/m2-snapshots + dav:https://apktool2.googlecode.com/svn/m2-snapshots diff --git a/pom.xml~ b/pom.xml~ new file mode 100644 index 00000000..fd408682 --- /dev/null +++ b/pom.xml~ @@ -0,0 +1,75 @@ + + + 4.0.0 + + brut.apktool + apktool-project + 1.0-SNAPSHOT + pom + + apktool + http://github.com/brutall/brut.apktool + + + UTF-8 + ${basedir} + + + + apktool-lib + apktool-cli + + + + + + .. + + LICENSE + NOTICE + NOTICE-smali + + + + + + + com.mycila.maven-license-plugin + maven-license-plugin + +
${root.basedir}/src/templates/apache2.0-header.txt
+ true + + .gitignore + LICENSE + NOTICE + NOTICE-smali + +
+ + + + check + + + +
+ + org.apache.maven.plugins + maven-compiler-plugin + + 6 + 6 + + +
+ + + + org.apache.maven.wagon + wagon-webdav + 1.0-beta-2 + + +
+