From 1880b592f4730208158ad4e4dfae9719b2c9d3ad Mon Sep 17 00:00:00 2001 From: AndnixSH <40742924+AndnixSH@users.noreply.github.com> Date: Thu, 2 Mar 2023 12:28:48 +0100 Subject: [PATCH] Implement split apk merging. Use own temp directory Apk merging is based on https://github.com/shadow578/ApksMerger --- APKToolGUI/APKToolGUI.csproj | 18 + .../AndroidRes/AndroidResourceMerger.cs | 562 ++++++++++++++++++ APKToolGUI/AndroidRes/AndroidResources.cs | 103 ++++ .../AndroidRes/Model/AndroidAttribute.cs | 15 + APKToolGUI/AndroidRes/Model/AndroidBool.cs | 12 + APKToolGUI/AndroidRes/Model/AndroidInteger.cs | 12 + APKToolGUI/AndroidRes/Model/AndroidPlural.cs | 21 + APKToolGUI/AndroidRes/Model/AndroidPublic.cs | 14 + APKToolGUI/AndroidRes/Model/AndroidString.cs | 18 + APKToolGUI/AndroidRes/Model/AndroidStyle.cs | 15 + .../AndroidRes/Model/AndroidStyleable.cs | 12 + .../AndroidRes/Model/AndroidTypedItem.cs | 15 + .../Model/Generic/AndroidGeneric.cs | 11 + .../Model/Generic/AndroidGenericArray.cs | 18 + .../Model/Generic/AndroidResource.cs | 22 + .../AndroidRes/Model/GenericArrayTypes.cs | 8 + APKToolGUI/AndroidRes/Model/GenericTypes.cs | 12 + APKToolGUI/AndroidRes/Util/ClassExtensions.cs | 115 ++++ APKToolGUI/AndroidRes/Util/Log.cs | 265 +++++++++ APKToolGUI/Forms/FormMain.cs | 200 ++++++- APKToolGUI/Forms/FormMain.resx | 4 +- APKToolGUI/Forms/FormSettings.cs | 4 +- .../Handlers/DecodeControlEventHandlers.cs | 10 +- APKToolGUI/Handlers/DragDropHandlers.cs | 59 +- APKToolGUI/Languages/Language.Designer.cs | 81 +++ APKToolGUI/Languages/Language.resx | 27 + APKToolGUI/Program.cs | 7 +- APKToolGUI/Utils/DirectoryUtils.cs | 13 +- APKToolGUI/Utils/DragDropUtils.cs | 123 ++-- APKToolGUI/Utils/PathUtils.cs | 11 + APKToolGUI/Utils/StringExt.cs | 11 + APKToolGUI/Utils/ZipUtils.cs | 9 + 32 files changed, 1688 insertions(+), 139 deletions(-) create mode 100644 APKToolGUI/AndroidRes/AndroidResourceMerger.cs create mode 100644 APKToolGUI/AndroidRes/AndroidResources.cs create mode 100644 APKToolGUI/AndroidRes/Model/AndroidAttribute.cs create mode 100644 APKToolGUI/AndroidRes/Model/AndroidBool.cs create mode 100644 APKToolGUI/AndroidRes/Model/AndroidInteger.cs create mode 100644 APKToolGUI/AndroidRes/Model/AndroidPlural.cs create mode 100644 APKToolGUI/AndroidRes/Model/AndroidPublic.cs create mode 100644 APKToolGUI/AndroidRes/Model/AndroidString.cs create mode 100644 APKToolGUI/AndroidRes/Model/AndroidStyle.cs create mode 100644 APKToolGUI/AndroidRes/Model/AndroidStyleable.cs create mode 100644 APKToolGUI/AndroidRes/Model/AndroidTypedItem.cs create mode 100644 APKToolGUI/AndroidRes/Model/Generic/AndroidGeneric.cs create mode 100644 APKToolGUI/AndroidRes/Model/Generic/AndroidGenericArray.cs create mode 100644 APKToolGUI/AndroidRes/Model/Generic/AndroidResource.cs create mode 100644 APKToolGUI/AndroidRes/Model/GenericArrayTypes.cs create mode 100644 APKToolGUI/AndroidRes/Model/GenericTypes.cs create mode 100644 APKToolGUI/AndroidRes/Util/ClassExtensions.cs create mode 100644 APKToolGUI/AndroidRes/Util/Log.cs diff --git a/APKToolGUI/APKToolGUI.csproj b/APKToolGUI/APKToolGUI.csproj index 7b7bc8e..e39303e 100644 --- a/APKToolGUI/APKToolGUI.csproj +++ b/APKToolGUI/APKToolGUI.csproj @@ -267,6 +267,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/APKToolGUI/AndroidRes/AndroidResourceMerger.cs b/APKToolGUI/AndroidRes/AndroidResourceMerger.cs new file mode 100644 index 0000000..62d9a46 --- /dev/null +++ b/APKToolGUI/AndroidRes/AndroidResourceMerger.cs @@ -0,0 +1,562 @@ +//https://github.com/shadow578/ApksMerger + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml; +using APKSMerger.AndroidRes.Model; +using APKSMerger.AndroidRes.Model.Generic; +using APKSMerger.Util; +using APKToolGUI.Utils; + +namespace APKSMerger.AndroidRes +{ + /// + /// merges android resource files + /// + public sealed class AndroidMerger + { + /// + /// check capabilities of the base and splits, warn if (common) libs are missing + /// + /// list of supported locales; key is locale, value is name of dir that first included it + /// list of supported abis; key is abi, value is name of dir that first included it + /// base project dir + /// split dirs + public void CollectCapabilities(out Dictionary locales, out Dictionary abis, + DirectoryInfo baseDir, params DirectoryInfo[] splits) + { + //init dicts + Log.i("collecting info about splits..."); + locales = new Dictionary(); + abis = new Dictionary(); + + //combine base and splits into one list + List allDir = new List(); + allDir.Add(baseDir); + allDir.AddRange(splits); + + //check all dirs, collect infos about them + foreach (DirectoryInfo d in allDir) + { + //check exists + if (!d.Exists) + { + Directory.CreateDirectory(baseDir.FullName); + Log.w($"Create baseDir {baseDir.FullName}"); + continue; + } + + //get all library archs included in this dir + //a decompiled apk dir may have a lib directory that contains native libraries for all archs supported by that apk (or split) + //the archs are splitted into their own directories, depending on the arch they're for + string libsDir = Path.Combine(d.FullName, "lib"); + if (Directory.Exists(libsDir)) + { + foreach (string arch in Directory.EnumerateDirectories(libsDir)) + { + //get name of arch + string archName = Path.GetFileName(arch); + + //add arch to lists of abis + if (!abis.ContainsKey(archName)) + { + Log.v($"{d.Name} includes abi {archName}"); + abis.Add(archName, d.Name); + } + else + { + //double arch? + Log.w($"arch {archName} already included by {abis[archName]} - in {d.Name}"); + } + } + } + else + { + Log.v($"{d.Name} does not include abis"); + } + + //get all locales included in this dir + //extra locales are defined in strings.xml files in directories named values- + //locale name seems to be formatted as ISO 639, but with an extra r (so en-GB == en-rGB) + string resDir = Path.Combine(d.FullName, "res"); + if (Directory.Exists(resDir)) + { + //add all dirs matching pattern (like values-en-rGB) + foreach (string lang in Directory.EnumerateDirectories(resDir, @"values-*")) + { + //check directory contains a strings.xml + if (!File.Exists(Path.Combine(lang, "strings.xml"))) + continue; + + //get name of lang + string langName = Path.GetFileName(lang).ReplaceFirst("values-", ""); + + //add lang to list of locales + if (!locales.ContainsKey(langName)) + { + Log.v($"{d.Name} included locale {langName}"); + locales.Add(langName, d.Name); + } + else + { + //double lang? + Log.w($"locale {langName} already included by {locales[langName]} - in {d.Name}"); + } + } + } + else + { + Log.v($"{d.Name} does not include locales"); + } + } + } + + /// + /// merge all splits into the base project dir + /// + /// base project dir + /// split dirs to merge + public void MergeSplits(DirectoryInfo baseDir, params DirectoryInfo[] splits) + { + //Log.v($"Base dir: {baseDir.FullName}"); + //check all dirs exists + if (!baseDir.Exists) + { + Directory.CreateDirectory(baseDir.FullName); + Log.w($"Create baseDir {baseDir.FullName}"); + //return; + } + + foreach (DirectoryInfo dir in splits) + { + //Debug.WriteLine(dir); + if (!dir.Exists) + { + Log.e($"split dir {dir.FullName} dos not exist!"); + return; + } + } + + List assetPacks = new List(); + //enumarate all splitted files + Dictionary globalNameReplacements = new Dictionary(); + foreach (DirectoryInfo split in splits) + { + //Log.v($"Split dir: {split.FullName}"); + split.EnumerateAllFiles("*.*", true, (FileInfo splittedFile) => + { + if (splittedFile.FullName.Contains("AndroidManifest.xml")) + { + string manifest = File.ReadAllText(splittedFile.FullName); + string splitModule = StringExt.Regex(@"(?<= split=\"")(.*?)(?=\"")", manifest); + + if (!String.IsNullOrEmpty(splitModule) && manifest.Contains("dist:type=\"asset-pack\"")) + { + Log.v($"Add module: {splitModule}"); + assetPacks.Add(splitModule); + } + } + + //Debug.WriteLine($"Splited file: {splittedFile.FullName}"); + //check if should process + string splitRel = PathUtils.GetRelativePath(split.FullName, splittedFile.FullName); + if (!ShouldProcess(splittedFile, split)) + { + //Log.v($"skip excluded split file {splitRel}"); + return; + } + + //Debug.WriteLine($"Split rel dir: {splitRel}"); + //Debug.WriteLine($"base Dir: {baseDir.FullName}"); + List splitList = splitRel.Split('\\').ToList(); + splitList.RemoveAt(0); + string outputString = string.Join("\\", splitList); + + //get file path for base dir + FileInfo baseFile = new FileInfo(Path.Combine(baseDir.FullName, outputString)); + //Debug.WriteLine($"Base file: {baseFile}"); + //Log.v($"Base file: {baseFile}"); + + //create target dir in base if needed + string baseFileDir = Path.GetDirectoryName(baseFile.FullName); + //Log.v($"Base file´dir: {baseFileDir}"); + //Debug.WriteLine($"Base file dir: {baseFileDir}"); + if (!Directory.Exists(baseFileDir)) + { + Directory.CreateDirectory(baseFileDir); + } + + //check file exists in base and is resource xml + if (!IsResourceXml(baseFile)) + { + //nothing to merge, just copy + Log.v($"Move split file {splitRel} to {baseFile}"); + if (File.Exists(baseFile.FullName)) + File.Delete(baseFile.FullName); + splittedFile.MoveTo(baseFile.FullName); + } + else + { + //already exists, merge + //Debug.WriteLine($"Merge split file {splitRel} with {baseFile}"); + + //skip if files are equal + if (baseFile.HasSameHash(splittedFile)) + { + Log.vv($"base and split of {splitRel} have same hash, skipping..."); + return; + } + + //check base and split are both resource xmls, if not skip + if (/*!IsResourceXml(baseFile) ||*/ !IsResourceXml(splittedFile)) + { + Log.vv($"split of {splitRel} is not resource xml, skipping..."); + return; + } + + if (splittedFile.FullName.Contains("styles.xml")) + { + Debug.WriteLine("Break"); + } + + //merge + MergeResourceXML(baseFile, splittedFile, globalNameReplacements); + } + }); + } + + //skip replacement if no global name replacements are available + if (globalNameReplacements.Count <= 0) + { + Log.d("skip global name replacements: count is 0"); + } + + //replace names globally (in xml only) + Log.d($"process {globalNameReplacements.Count} global name replacements..."); + foreach (string org in globalNameReplacements.Keys) + { + Log.v($"Replace {org} with {globalNameReplacements[org]}"); + //Debug.Write($"replace {org} with {globalNameReplacements[org]}"); + } + + baseDir.EnumerateAllFiles("*.xml", true, (FileInfo file) => + { + //Debug.WriteLine($"Name replace in {file.FullName}"); + + //create temp file + FileInfo temp = new FileInfo(Path.GetTempFileName()); + + //copy from input to temp, replace everything on replace list + using (StreamReader inp = file.OpenText()) + using (StreamWriter oup = temp.CreateText()) + { + + string ln; + while ((ln = inp.ReadLine()) != null) + { + //replace all + foreach (string org in globalNameReplacements.Keys) + { + string dummy = StringExt.Regex(@"APKTOOL_DUMMY_([A-Za-z0-9])\w", ln); + + if (ln.Contains(dummy)) + { + //To avoid replacing wrong dummies. Don't know if there is better way + ln = ln.Replace(org + "<", globalNameReplacements[org] + "<"); + ln = ln.Replace(org + "\"", globalNameReplacements[org] + "\""); + //Debug.WriteLine($"Replaced {org} with {globalNameReplacements[org]} in {file.FullName}"); + } + //if (ln.Contains(org)) + // ln = Regex.Replace(ln, @"APKTOOL_DUMMY_([A-Za-z0-9])\w", globalNameReplacements[org]); + } + + //write back + oup.WriteLine(ln); + } + } + + //move temp to input and delete temp if still exists + string tempPath = temp.FullName; + + if (File.Exists(file.FullName)) + { + File.Delete(file.FullName); + } + + temp.MoveTo(file.FullName); + + if (File.Exists(tempPath)) + { + File.Delete(tempPath); + } + }); + + //remove splits from android manifest + FileInfo baseManifest = new FileInfo(Path.Combine(baseDir.FullName, "AndroidManifest.xml")); + PatchManifest(baseManifest, assetPacks); + + FileInfo baseYml = new FileInfo(Path.Combine(baseDir.FullName, "apktool.yml")); + PatchYml(baseYml, assetPacks); + } + + /// + /// Patch the AndroidManifest.xml to not use splits + /// + /// the manifest xml to patch + void PatchManifest(FileInfo manifest, List assetPacks) + { + Log.d($"patching manifest {manifest.FullName}..."); + + //check the file exists + if (!manifest.Exists) + { + Log.e("manifest to patch does not exist!"); + return; + } + + string modules = null; + if (assetPacks.Count != 0) + { + foreach (string asset in assetPacks) + { + modules += "," + asset; + } + } + + //prepare targets to remove + string[] replaceTargets = { @"android:isSplitRequired=""true""" }; + + List removeTargets = new List { @"meta-data android:name=""com.android.stamp.source""", + @"meta-data android:name=""com.android.vending.derived.apk.id""", + @"meta-data android:name=""com.android.vending.splits.required""", + @"meta-data android:name=""com.android.vending.splits"""}; + + //create temp file + FileInfo temp = new FileInfo(Path.GetTempFileName()); + + //copy from input to temp, replace everything on replace list + using (StreamReader inp = manifest.OpenText()) + using (StreamWriter oup = temp.CreateText()) + { + string ln; + while ((ln = inp.ReadLine()) != null) + { + //remove all + foreach (string target in replaceTargets) + { + ln = ln.Replace(target, ""); + } + + if (removeTargets.Any(w => ln.Contains(w))) + continue; + + if (ln.Contains("STAMP_TYPE_DISTRIBUTION_APK")) + ln = ln.Replace("STAMP_TYPE_DISTRIBUTION_APK", "STAMP_TYPE_STANDALONE_APK"); + + if (ln.Contains("") && !String.IsNullOrEmpty(modules)) + { + oup.WriteLine(@" "); + } + + //write back + oup.WriteLine(ln); + } + } + + //move temp to input and delete temp if still exists + string tempPath = temp.FullName; + + if (File.Exists(manifest.FullName)) + { + File.Delete(manifest.FullName); + } + + temp.MoveTo(manifest.FullName); + + if (File.Exists(tempPath)) + { + File.Delete(tempPath); + } + } + + void PatchYml(FileInfo yml, List assetPacks) + { + Log.d($"patching apktool.yml {yml.FullName}..."); + + //check the file exists + if (!yml.Exists) + { + Log.e("manifest to patch does not exist!"); + return; + } + + //create temp file + FileInfo temp = new FileInfo(Path.GetTempFileName()); + + //copy from input to temp, replace everything on replace list + using (StreamReader inp = yml.OpenText()) + using (StreamWriter oup = temp.CreateText()) + { + string ln; + while ((ln = inp.ReadLine()) != null) + { + if (ln.Contains("doNotCompress:") && assetPacks.Count != 0) + { + oup.WriteLine(ln); + foreach (string asset in assetPacks) + { + oup.WriteLine("- assets/assetpack/" + asset); + } + continue; + } + + //write back + oup.WriteLine(ln); + } + } + + //move temp to input and delete temp if still exists + string tempPath = temp.FullName; + + if (File.Exists(yml.FullName)) + { + File.Delete(yml.FullName); + } + + temp.MoveTo(yml.FullName); + + if (File.Exists(tempPath)) + { + File.Delete(tempPath); + } + } + + /// + /// merge two splitted resource xmls, overwrite a with merged + /// + /// file a to merge + /// file b to merge + /// dictionary that can be used to replace names of resources globally + void MergeResourceXML(FileInfo a, FileInfo b, Dictionary globalNameReplacements) + { + //deserialize both + AndroidResources resBase = AndroidResources.FromFile(a.FullName); + AndroidResources resSplit = AndroidResources.FromFile(b.FullName); + + //merge resources to resA + foreach (AndroidResource res in resSplit.Values) + { + if (res is AndroidPublic splitP) + { + //entry of public.xml, special merge (Id has to be unique) + //try to find public with same id in base apk + AndroidPublic baseP = resBase.FindPublicWithId(splitP.Id); + if (baseP == null || !baseP.Type.Equals(splitP.Type)) + { + //id not found or wrong type, add from split + resBase.Values.Add(splitP); + } + else + { + //id with correct type found in base, + //check if name of base is apktool dummy and name of split is not + if (baseP.Name.StartsWith("APKTOOL_DUMMY") && !splitP.Name.StartsWith("APKTOOL_DUMMY")) + { + try + { + Log.v($"Replace {baseP.Name} with {splitP.Name}..."); + globalNameReplacements.Add(baseP.Name, splitP.Name); + baseP.Name = splitP.Name; + } + catch (Exception ex) + { + Log.v($"Error replacing {baseP.Name} with {splitP.Name}..."); + Debug.WriteLine(ex.Message); + } + } + } + } + else + { + //normal resource entry (string / color / ...) + if (!resBase.Values.Contains(res)) + { + + resBase.Values.Add(res); + } + } + } + + //serialize back to a + resBase.ToFile(a.FullName); + } + + /// + /// check if the xml file contains the resources xml tag + /// + /// the xml to check + /// does the xml contain the tag? + bool IsResourceXml(FileInfo f) + { + //check file exists + if (!f.Exists) return false; + + try + { + //Net reactor cause error + //check xml root + XmlDocument xml = new XmlDocument(); + + xml.Load(f.FullName); + //Log.v($"IsResourceXml 5"); + return xml.DocumentElement.Name.Equals("resources", StringComparison.OrdinalIgnoreCase); + } + catch + { + //probably bad xml + return false; + } + } + + /// + /// should the file be processed? + /// Example for files to exclude from processing are AndroidManifest.xml, apktool.yml, and META-INF/* + /// + /// the file to check + /// the project dir the file is in + /// process the file? + bool ShouldProcess(FileInfo file, DirectoryInfo projDir) + { + //get relative path + string filePathRel = PathUtils.GetRelativePath(projDir.FullName, file.FullName).TrimStart('/').TrimStart('\\'); + + //check if in META-INF (exclude all) + //if (filePathRel.StartsWith("META-INF", StringComparison.OrdinalIgnoreCase)) + // return false; + + //check if in original (exlude all) + if (filePathRel.StartsWith("original", StringComparison.OrdinalIgnoreCase)) + return false; + + //check if AndroidManifest.xml OR apktool.yml + if (file.Name.Equals("androidmanifest.xml", StringComparison.OrdinalIgnoreCase) + || file.Name.Equals("apktool.yml", StringComparison.OrdinalIgnoreCase)) + return false; + + //check if AndroidManifest.xml OR apktool.yml + if (file.Name.Equals("resources.arsc", StringComparison.OrdinalIgnoreCase)) + return false; + + //check if drawables.yml + //if (file.Name.Equals("drawables.xml", StringComparison.OrdinalIgnoreCase)) + // return false; + + //all ok, include + return true; + } + } +} diff --git a/APKToolGUI/AndroidRes/AndroidResources.cs b/APKToolGUI/AndroidRes/AndroidResources.cs new file mode 100644 index 0000000..5d484a8 --- /dev/null +++ b/APKToolGUI/AndroidRes/AndroidResources.cs @@ -0,0 +1,103 @@ +//https://github.com/shadow578/ApksMerger + +using APKSMerger.AndroidRes.Model; +using APKSMerger.AndroidRes.Model.Generic; +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes +{ + [XmlRoot("resources")] + public sealed class AndroidResources + { + //basic + [XmlElement("bool", Type = typeof(AndroidBool))] + [XmlElement("integer", Type = typeof(AndroidInteger))] + [XmlElement("dimen", Type = typeof(AndroidDimension))] + [XmlElement("drawable", Type = typeof(AndroidDrawable))] + [XmlElement("color", Type = typeof(AndroidColor))] + [XmlElement("fraction", Type = typeof(AndroidFraction))] + + //extended + [XmlElement("attr", Type = typeof(AndroidAttribute))] + [XmlElement("string", Type = typeof(AndroidString))] + [XmlElement("item", Type = typeof(AndroidTypedItem))] + [XmlElement("public", Type = typeof(AndroidPublic))] + + //complex + [XmlElement("style", Type = typeof(AndroidStyle))] + [XmlElement("plurals", Type = typeof(AndroidPlural))] + [XmlElement("string-array", Type = typeof(AndroidStringArray))] + [XmlElement("integer-array", Type = typeof(AndroidIntegerArray))] + [XmlElement("array", Type = typeof(AndroidGenericArray))] + [XmlElement("declare-styleable", Type = typeof(AndroidStyleable))] + public List Values { get; set; } = new List(); + + /// + /// Find a AndroidPublic with matching id + /// + /// the id to find + /// matching public, or null if not found + public AndroidPublic FindPublicWithId(string id) + { + foreach(AndroidResource res in Values) + { + if((res is AndroidPublic pub) && pub.Id.Equals(id)) + { + return pub; + } + } + + return null; + } + + /// + /// Deserialize a file into a object + /// + /// the file to deserialize + /// the object + public static AndroidResources FromFile(string file) + { + //check file + if (!File.Exists(file)) return null; + + //deserialize + try + { + XmlSerializer ser = new XmlSerializer(typeof(AndroidResources)); + using (StreamReader reader = File.OpenText(file)) + { + return ser.Deserialize(reader) as AndroidResources; + } + } + catch + { + return null; + } + } + + /// + /// serialize into a file + /// + /// the file to serialize to, will be overwritten if exists + /// write file ok? + public bool ToFile(string file) + { + try + { + XmlSerializer ser = new XmlSerializer(typeof(AndroidResources)); + using (StreamWriter writer = File.CreateText(file)) + { + ser.Serialize(writer, this, new XmlSerializerNamespaces()); + return true; + } + } + catch + { + return false; + } + } + } +} diff --git a/APKToolGUI/AndroidRes/Model/AndroidAttribute.cs b/APKToolGUI/AndroidRes/Model/AndroidAttribute.cs new file mode 100644 index 0000000..dff15ec --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/AndroidAttribute.cs @@ -0,0 +1,15 @@ +using APKSMerger.AndroidRes.Model.Generic; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model +{ + public sealed class AndroidAttribute : AndroidResource + { + [XmlAttribute("format")] + public string Format { get; set; } + + //[XmlText] + //[XmlAttribute("value")] + public string Value { get; set; } + } +} diff --git a/APKToolGUI/AndroidRes/Model/AndroidBool.cs b/APKToolGUI/AndroidRes/Model/AndroidBool.cs new file mode 100644 index 0000000..2b4a6a4 --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/AndroidBool.cs @@ -0,0 +1,12 @@ +using APKSMerger.AndroidRes.Model.Generic; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model +{ + public sealed class AndroidBool : AndroidResource + { + //[XmlText] + //[XmlAttribute("value")] + public bool Value { get; set; } + } +} diff --git a/APKToolGUI/AndroidRes/Model/AndroidInteger.cs b/APKToolGUI/AndroidRes/Model/AndroidInteger.cs new file mode 100644 index 0000000..3d5cd1c --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/AndroidInteger.cs @@ -0,0 +1,12 @@ +using APKSMerger.AndroidRes.Model.Generic; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model +{ + public sealed class AndroidInteger : AndroidResource + { + //[XmlText] + //[XmlAttribute("value")] + public int Value { get; set; } + } +} diff --git a/APKToolGUI/AndroidRes/Model/AndroidPlural.cs b/APKToolGUI/AndroidRes/Model/AndroidPlural.cs new file mode 100644 index 0000000..0cce3aa --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/AndroidPlural.cs @@ -0,0 +1,21 @@ +using APKSMerger.AndroidRes.Model.Generic; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model +{ + public sealed class AndroidPlural : AndroidResource + { + public sealed class Plural + { + [XmlAttribute("quantitiy")] + public string Quantity { get; set; } + + [XmlText] + public string Value { get; set; } + } + + [XmlElement("item", Type = typeof(Plural))] + public List Values { get; set; } = new List(); + } +} diff --git a/APKToolGUI/AndroidRes/Model/AndroidPublic.cs b/APKToolGUI/AndroidRes/Model/AndroidPublic.cs new file mode 100644 index 0000000..c5dbde0 --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/AndroidPublic.cs @@ -0,0 +1,14 @@ +using APKSMerger.AndroidRes.Model.Generic; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model +{ + public sealed class AndroidPublic : AndroidResource + { + [XmlAttribute("type")] + public string Type { get; set; } + + [XmlAttribute("id")] + public string Id { get; set; } + } +} diff --git a/APKToolGUI/AndroidRes/Model/AndroidString.cs b/APKToolGUI/AndroidRes/Model/AndroidString.cs new file mode 100644 index 0000000..f9cc52a --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/AndroidString.cs @@ -0,0 +1,18 @@ +using APKSMerger.AndroidRes.Model.Generic; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model +{ + public sealed class AndroidString : AndroidResource + { + //[XmlAttribute("formatted")] + //public bool Formatted { get; set; } + + //[XmlAttribute("translatable")] + //public bool Translateable { get; set; } + + // [XmlText] + //[XmlAttribute("value")] + public string Value { get; set; } + } +} diff --git a/APKToolGUI/AndroidRes/Model/AndroidStyle.cs b/APKToolGUI/AndroidRes/Model/AndroidStyle.cs new file mode 100644 index 0000000..b5d01ca --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/AndroidStyle.cs @@ -0,0 +1,15 @@ +using APKSMerger.AndroidRes.Model.Generic; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model +{ + public sealed class AndroidStyle : AndroidResource + { + [XmlAttribute("parent")] + public string Parent { get; set; } + + [XmlElement("item", Type = typeof(AndroidGeneric))] + public List Items { get; set; } = new List(); + } +} diff --git a/APKToolGUI/AndroidRes/Model/AndroidStyleable.cs b/APKToolGUI/AndroidRes/Model/AndroidStyleable.cs new file mode 100644 index 0000000..63914bc --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/AndroidStyleable.cs @@ -0,0 +1,12 @@ +using APKSMerger.AndroidRes.Model.Generic; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model +{ + public sealed class AndroidStyleable : AndroidResource + { + [XmlElement("attr", Type = typeof(AndroidAttribute))] + public List Values { get; set; } = new List(); + } +} diff --git a/APKToolGUI/AndroidRes/Model/AndroidTypedItem.cs b/APKToolGUI/AndroidRes/Model/AndroidTypedItem.cs new file mode 100644 index 0000000..e8e6c2e --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/AndroidTypedItem.cs @@ -0,0 +1,15 @@ +using APKSMerger.AndroidRes.Model.Generic; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model +{ + public sealed class AndroidTypedItem : AndroidResource + { + [XmlAttribute("type")] + public string Type { get; set; } + + //[XmlText] + //[XmlAttribute("value")] + public string Value { get; set; } + } +} diff --git a/APKToolGUI/AndroidRes/Model/Generic/AndroidGeneric.cs b/APKToolGUI/AndroidRes/Model/Generic/AndroidGeneric.cs new file mode 100644 index 0000000..6fcf621 --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/Generic/AndroidGeneric.cs @@ -0,0 +1,11 @@ +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model.Generic +{ + public class AndroidGeneric : AndroidResource + { + [XmlText] + //[XmlAttribute("value")] + public string Value { get; set; } + } +} diff --git a/APKToolGUI/AndroidRes/Model/Generic/AndroidGenericArray.cs b/APKToolGUI/AndroidRes/Model/Generic/AndroidGenericArray.cs new file mode 100644 index 0000000..80279b8 --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/Generic/AndroidGenericArray.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model.Generic +{ + public class AndroidGenericArray : AndroidResource + { + public sealed class Item + { + // [XmlText] + //[XmlAttribute("value")] + public string Value { get; set; } + } + + [XmlElement("item", Type = typeof(Item))] + public List Values { get; set; } = new List(); + } +} diff --git a/APKToolGUI/AndroidRes/Model/Generic/AndroidResource.cs b/APKToolGUI/AndroidRes/Model/Generic/AndroidResource.cs new file mode 100644 index 0000000..744946e --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/Generic/AndroidResource.cs @@ -0,0 +1,22 @@ +using System; +using System.Diagnostics; +using System.Xml.Serialization; + +namespace APKSMerger.AndroidRes.Model.Generic +{ + public class AndroidResource + { + [XmlAttribute(AttributeName = "name")] + public string Name { get; set; } + + public override bool Equals(object obj) + { + //check other object is of correct type, otherwise not equal + if (!(obj is AndroidResource other)) return false; + + //check if name is equal + //Debug.WriteLine("Xml name: " + other.Name); + return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/APKToolGUI/AndroidRes/Model/GenericArrayTypes.cs b/APKToolGUI/AndroidRes/Model/GenericArrayTypes.cs new file mode 100644 index 0000000..0ccf9ed --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/GenericArrayTypes.cs @@ -0,0 +1,8 @@ +using APKSMerger.AndroidRes.Model.Generic; + +namespace APKSMerger.AndroidRes.Model +{ + public sealed class AndroidStringArray : AndroidGenericArray { } + + public sealed class AndroidIntegerArray : AndroidGenericArray { } +} diff --git a/APKToolGUI/AndroidRes/Model/GenericTypes.cs b/APKToolGUI/AndroidRes/Model/GenericTypes.cs new file mode 100644 index 0000000..6cfd881 --- /dev/null +++ b/APKToolGUI/AndroidRes/Model/GenericTypes.cs @@ -0,0 +1,12 @@ +using APKSMerger.AndroidRes.Model.Generic; + +namespace APKSMerger.AndroidRes.Model +{ + public sealed class AndroidDimension : AndroidGeneric { } + + public sealed class AndroidDrawable : AndroidGeneric { } + + public sealed class AndroidColor : AndroidGeneric { } + + public sealed class AndroidFraction : AndroidGeneric { } +} diff --git a/APKToolGUI/AndroidRes/Util/ClassExtensions.cs b/APKToolGUI/AndroidRes/Util/ClassExtensions.cs new file mode 100644 index 0000000..2e6862e --- /dev/null +++ b/APKToolGUI/AndroidRes/Util/ClassExtensions.cs @@ -0,0 +1,115 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Threading.Tasks; + +namespace APKSMerger.Util +{ + /// + /// extension methods + /// + public static class ClassExtensions + { + /// + /// replaces the first occurance of the pattern with the replacement + /// + /// the string to replace in + /// the pattern to replace + /// the replacement for the pattern + /// a string in wich the first occurance of the pattern was replaced + public static string ReplaceFirst(this string s, string pattern, string replacement) + { + int pos = s.IndexOf(pattern); + if (pos < 0) + { + return s; + } + return s.Substring(0, pos) + replacement + s.Substring(pos + pattern.Length); + } + + /// + /// does the array contain the string a, ignoring case? + /// + /// the array to check + /// the string to check for + /// contains it? + public static bool ContainsIgnoreCase(this string[] s, string a) + { + foreach (string sa in s) + { + if (sa.Equals(a, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + /// + /// enumerates all files in the directory (and subdirs if enabled) + /// + /// the directory to enumerate in + /// the pattern to filter with, eg. *.* or *.txt + /// should files in subdirs be included? + /// the action to execute for all files + public static void EnumerateAllFiles(this DirectoryInfo dir, string pattern, bool includeSubDirs, Action action) + { + foreach (FileInfo file in dir.EnumerateFiles(pattern, includeSubDirs ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)) + { + action.Invoke(file); + } + } + + /// + /// enumerates all files in the directory (and subdirs if enabled) in parallel + /// + /// the directory to enumerate in + /// the pattern to filter with, eg. *.* or *.txt + /// should files in subdirs be included? + /// the action to execute for all files + public static void EnumerateAllFilesParallel(this DirectoryInfo dir, string pattern, bool includeSubDirs, Action action) + { + Parallel.ForEach(dir.EnumerateFiles(pattern, includeSubDirs ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly), action); + } + + /// + /// checks if the two files have the same hash (MD5) + /// + /// the first file + /// the file to compare + /// do they have the same hash? + public static bool HasSameHash(this FileInfo a, FileInfo b) + { + return a.GetMD5().Equals(b.GetMD5()); + } + + /// + /// Get the md5 of the file + /// + /// the file to get md5 of + /// md5 string of the file + public static string GetMD5(this FileInfo f) + { + using (MD5 md5 = MD5.Create()) + using (FileStream stream = f.OpenRead()) + { + return BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", "").ToLowerInvariant(); + } + } + + /// + /// repeat the char n times + /// + /// char to repeat + /// how often to repeat + /// string with n time c + public static string Repeat(this char c, int n) + { + string s = ""; + for (int i = 0; i < n; i++) + s += c; + + return s; + } + } +} diff --git a/APKToolGUI/AndroidRes/Util/Log.cs b/APKToolGUI/AndroidRes/Util/Log.cs new file mode 100644 index 0000000..5583160 --- /dev/null +++ b/APKToolGUI/AndroidRes/Util/Log.cs @@ -0,0 +1,265 @@ +using APKToolGUI; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Windows.Media; + +namespace APKSMerger.Util +{ + /// + /// simple logging wrapper class + /// + public static class Log + { + /// + /// should very verbose logs (Log.vv) be written? + /// + public static bool LogVeryVerbose { get; set; } = false; + + /// + /// should verbose logs (Log.v) be written? + /// + public static bool LogVerbose { get; set; } = true; + + /// + /// should debug logs (Log.d) be written? + /// + public static bool LogDebug { get; set; } = true; + + #region direct logs + /// + /// log message with level VERY VERBOSE (may be disabled) + /// + /// the string to log + public static void vv(string s) + { + if (!LogVeryVerbose) return; + FormMain.Instance.ToLog(ApktoolEventType.None, s); + } + + /// + /// log message with level VERBOSE (may be disabled) + /// + /// the string to log + public static void v(string s) + { + if (!LogVerbose) return; + FormMain.Instance.ToLog(ApktoolEventType.None, s); + } + + /// + /// log message with level DEBUG (may be disabled) + /// + /// the string to log + public static void d(string s) + { + if (!LogDebug) return; + FormMain.Instance.ToLog(ApktoolEventType.None, s); + } + + /// + /// log message with level INFO + /// + /// the string to log + public static void i(string s) + { + FormMain.Instance.ToLog(ApktoolEventType.Infomation, s); + } + + /// + /// log message with level WARNING + /// + /// the string to log + public static void w(string s) + { + FormMain.Instance.ToLog(ApktoolEventType.Warning, s); + } + + /// + /// log message with level ERROR + /// + /// the string to log + public static void e(string s) + { + FormMain.Instance.ToLog(ApktoolEventType.Error, s); + } + #endregion + + /// + /// start a new async log session + /// + /// + public static AsyncLogSession StartAsync() + { + return new AsyncLogSession(); + } + + /// + /// writes a direct log message + /// + /// the string to log + /// color to log in, null is default + static void WriteLogDirect(string s, ConsoleColor? color = null) + { + //set color + ConsoleColor iColor = Console.ForegroundColor; + if (color.HasValue) + { + Console.ForegroundColor = color.Value; + } + + //write log + Console.WriteLine(s); + + //restore color + if (color.HasValue) + { + Console.ForegroundColor = iColor; + } + } + + /// + /// async log sesssion + /// + public class AsyncLogSession : IDisposable + { + /// + /// lock object to ensure only one object commits at a time + /// + static readonly object _CommitLock = new object(); + + /// + /// Tag to include when logging + /// + Stack tags = new Stack(); + + /// + /// contains all pending log entries + /// + Queue pending = new Queue(); + + #region log functions + /// + /// log message with level VERY VERBOSE (may be disabled) + /// + /// the string to log + public void vv(string s) + { + if (!LogVeryVerbose) return; + + EnqueueMessage($"[VV]{s}"); + } + + /// + /// log message with level VERBOSE (may be disabled) + /// + /// the string to log + public void v(string s) + { + if (!LogVerbose) return; + + EnqueueMessage($"[V]{s}"); + } + + /// + /// log message with level DEBUG (may be disabled) + /// + /// the string to log + public void d(string s) + { + if (!LogDebug) return; + + EnqueueMessage($"[D]{s}"); + } + + /// + /// log message with level INFO + /// + /// the string to log + public void i(string s) + { + EnqueueMessage($"[I]{s}"); + } + + /// + /// log message with level WARNING + /// + /// the string to log + public void w(string s) + { + EnqueueMessage($"[W]{s}"); + } + + /// + /// log message with level ERROR + /// + /// the string to log + public void e(string s) + { + EnqueueMessage($"[E]{s}"); + } + #endregion + + /// + /// enqueue a message in the message queue + /// + /// the message to enqueue + void EnqueueMessage(string s) + { + pending.Enqueue($"{GetTag()}:{s}"); + } + + /// + /// push a tag onto the tags stack + /// + /// the tag to push + public void PushTag(string t) + { + tags.Push(t); + } + + /// + /// pop the last tag of the tags stack + /// + public void PopTag() + { + // tags.TryPop(out _); + } + + /// + /// get the current tag + /// + /// current tag, or string.Empty if no tag + public string GetTag() + { + string tag; + // if (!tags.TryPeek(out tag)) + tag = string.Empty; + + return tag; + } + + /// + /// commit all pending log entries + /// + public void Commit() + { + lock (_CommitLock) + { + while (pending.Count > 0) + { + WriteLogDirect(pending.Dequeue()); + } + } + } + + /// + /// same as calling .Commit(). used for using() syntax + /// + public void Dispose() + { + Commit(); + } + } + } +} diff --git a/APKToolGUI/Forms/FormMain.cs b/APKToolGUI/Forms/FormMain.cs index 0aa07a3..42d722f 100644 --- a/APKToolGUI/Forms/FormMain.cs +++ b/APKToolGUI/Forms/FormMain.cs @@ -14,7 +14,8 @@ using System.Collections.Generic; using APKToolGUI.Handlers; using Microsoft.WindowsAPICodePack.Taskbar; using System.Media; - +using APKSMerger.AndroidRes; +using Ionic.Zip; namespace APKToolGUI { @@ -34,8 +35,12 @@ namespace APKToolGUI private Stopwatch stopwatch; private string lastStartedDate; + internal static FormMain Instance { get; private set; } + public FormMain() { + Instance = this; + Program.SetLanguage(); InitializeComponent(); this.Text += " - v" + ProductVersion; @@ -113,6 +118,8 @@ namespace APKToolGUI else ToLog(ApktoolEventType.None, Language.DragDropSupported); + ToLog(ApktoolEventType.None, String.Format(Language.TempDirectory, Program.TempDirectory())); + TimeSpan updateInterval = DateTime.Now - Settings.Default.LastUpdateCheck; if (updateInterval.Days > 0 && Settings.Default.CheckForUpdateAtStartup) updateCheker.CheckAsync(true); @@ -140,8 +147,16 @@ namespace APKToolGUI switch (Environment.GetCommandLineArgs()[1]) { case "decapk": - if (await Decompile(file) == 0) - Close(); + if (file.ContainsAny(".xapk", ".zip", ".apks", ".apkm")) + { + if (await MergeAPK(file) == 0) + Close(); + } + else + { + if (await Decompile(file) == 0) + Close(); + } break; case "comapk": if (await Build(file) == 0) @@ -232,6 +247,49 @@ namespace APKToolGUI try { + string splitPath = Path.Combine(Program.TempDirectory(), "SplitInfo"); + string arch = null; + + await Task.Factory.StartNew(() => + { + DirectoryUtils.Delete(splitPath); + if (file.ContainsAny(".xapk", ".zip", ".apks", ".apkm")) + { + Directory.CreateDirectory(splitPath); + + using (ZipFile zipDest = ZipFile.Read(file)) + { + bool mainApkFound = false; + foreach (ZipEntry entry in zipDest.Entries) + { + if (!mainApkFound && !entry.FileName.Contains("config.") && entry.FileName.EndsWith(".apk")) + { + Debug.WriteLine("Found main APK" + entry.FileName); + entry.Extract(splitPath, ExtractExistingFileAction.OverwriteSilently); + file = Path.Combine(splitPath, entry.FileName); + mainApkFound = true; + } + if (entry.FileName.Contains("lib/armeabi-v7a")) + { + arch += "armeabi-v7a, "; + } + if (entry.FileName.Contains("lib/arm64-v8a")) + { + arch += "arm64-v8a, "; + } + if (entry.FileName.Contains("lib/x86")) + { + arch += "x86, "; + } + if (entry.FileName.Contains("lib/x86_64")) + { + arch += "x86_64, "; + } + } + } + } + }); + bool parsed = false; await Task.Factory.StartNew(() => { @@ -258,21 +316,26 @@ namespace APKToolGUI permTxtBox.Text = aapt.Permissions; localsTxtBox.Text = aapt.Locales; fullInfoTextBox.Text = aapt.FullInfo; - archSdkTxtBox.Text = aapt.NativeCode; + if (!String.IsNullOrEmpty(aapt.NativeCode)) + archSdkTxtBox.Text = aapt.NativeCode; + else + archSdkTxtBox.Text = arch.RemoveLast(", "); launchActivityTxtBox.Text = aapt.LaunchableActivity; if (aapt.AppIcon != null) { await Task.Factory.StartNew(() => { - ZipUtils.ExtractFile(file, aapt.AppIcon, Path.Combine(Program.TEMP_PATH, aapt.PackageName)); + ZipUtils.ExtractFile(file, aapt.AppIcon, Path.Combine(Program.TempDirectory(), aapt.PackageName)); }); - string icon = Path.Combine(Program.TEMP_PATH, aapt.PackageName, Path.GetFileName(aapt.AppIcon)); + string icon = Path.Combine(Program.TempDirectory(), aapt.PackageName, Path.GetFileName(aapt.AppIcon)); if (File.Exists(icon)) { apkIconPicBox.Image = BitmapUtils.LoadBitmap(icon); } } + + DirectoryUtils.Delete(splitPath); } } catch (Exception ex) @@ -283,9 +346,8 @@ namespace APKToolGUI ToLog(ApktoolEventType.Warning, Language.ErrorGettingApkInfo); #endif } - ToLog(ApktoolEventType.Done, Language.Done); - Done(); + ToStatus(Language.Done, Resources.done); } } #endregion @@ -434,6 +496,106 @@ namespace APKToolGUI } #endregion + #region Merge APK + internal async Task MergeAPK(string inputSplitApk) + { + int code = 0; + + string apkFileName = Path.GetFileName(inputSplitApk); + + string tempApk = Path.Combine(Program.TempDirectory(), "dec.apk"); + + string extractedSplitDir = Path.Combine(Program.TempDirectory(), "SplitApk"); + string decompileDir = Path.Combine(Program.TempDirectory(), "Decompiled"); + string mergedDir = Path.Combine(Program.TempDirectory(), "Merged"); + + string outputDir = PathUtils.GetDirectoryNameWithoutExtension(inputSplitApk); + if (Settings.Default.Decode_UseOutputDir && !IgnoreOutputDirContextMenu) + outputDir = Path.Combine(Settings.Default.Decode_OutputDir, Path.GetFileNameWithoutExtension(inputSplitApk)); + + try + { + Running(); + + DirectoryUtils.Delete(extractedSplitDir); + Directory.CreateDirectory(extractedSplitDir); + DirectoryUtils.Delete(mergedDir); + Directory.CreateDirectory(mergedDir); + + await Task.Factory.StartNew(() => + { + if (Settings.Default.Framework_ClearBeforeDecode) + { + if (ClearFramework() == 0) + ToLog(ApktoolEventType.None, Language.FrameworkCacheCleared); + else + ToLog(ApktoolEventType.Error, Language.ErrorClearingFw); + } + + ToLog(ApktoolEventType.Infomation, "=====[ " + Language.MergingApk + " ]====="); + ToLog(ApktoolEventType.None, String.Format(Language.InputFile, inputSplitApk)); + ToStatus(String.Format(Language.MergingApk + " \"{0}\"...", Path.GetFileName(inputSplitApk)), Resources.waiting); + + //Extract all apk files + ToLog(ApktoolEventType.None, Language.ExtractingAllApkFiles); + ZipUtils.ExtractAll(inputSplitApk, extractedSplitDir, true); + + //Decompile all apk files + ToLog(ApktoolEventType.None, Language.DecompilingAllApkFiles); + + List splitDirs = new List(); + var apkfiles = Directory.EnumerateFiles(extractedSplitDir, "*.apk"); + + foreach (string apk in apkfiles) + { + string output = Path.Combine(decompileDir, Path.GetFileNameWithoutExtension(apk)); + + code = apktool.Decompile(apk, output); + if (code != 0) + { + ToLog(ApktoolEventType.Error, Language.ErrorDecompiling); + throw new Exception(); + } + + if (Directory.Exists(Path.Combine(output, "smali")) || File.Exists(Path.Combine(output, "classes.dex"))) + { + ToLog(ApktoolEventType.Infomation, String.Format(Language.DetectedAsBase, apk)); + + ToLog(ApktoolEventType.None, String.Format(Language.MovingBasedirectory, decompileDir)); + DirectoryUtils.Move(output, mergedDir); + continue; + } + + DirectoryInfo splitI = new DirectoryInfo(output); + ToLog(ApktoolEventType.Infomation, String.Format(Language.DetectedAsSplit, apk)); + splitDirs.Add(splitI); + } + + AndroidMerger merger = new AndroidMerger(); + DirectoryInfo baseDir = new DirectoryInfo(mergedDir); + + Dictionary locales, abis; + + ToLog(ApktoolEventType.None, Language.MergingApk); + merger.CollectCapabilities(out locales, out abis, baseDir, splitDirs.ToArray()); + merger.MergeSplits(baseDir, splitDirs.ToArray()); + + ToLog(ApktoolEventType.None, Language.MergeFinishedMoveDir); + DirectoryUtils.Move(mergedDir, outputDir); + }); + } + catch (Exception ex) + { + code = 1; + ToLog(ApktoolEventType.Error, ex.ToString()); + } + + Done(printTimer: true); + + return code; + } + #endregion + #region Apktool private void InitializeAPKTool() { @@ -464,7 +626,7 @@ namespace APKToolGUI if (Settings.Default.Decode_UseOutputDir && !IgnoreOutputDirContextMenu) outputDir = Path.Combine(Settings.Default.Decode_OutputDir, Path.GetFileNameWithoutExtension(inputApk)); - string tempApk = Path.Combine(Program.TEMP_PATH, "dec.apk"); + string tempApk = Path.Combine(Program.TempDirectory(), "dec.apk"); string outputTempDir = tempApk.Replace(".apk", ""); try @@ -584,7 +746,7 @@ namespace APKToolGUI } string outputCompiledApkFile = outputFile; - string tempDecApkFolder = Path.Combine(Program.TEMP_PATH, "dec"); + string tempDecApkFolder = Path.Combine(Program.TempDirectory(), "dec"); string outputTempApk = tempDecApkFolder + ".apk"; if (Settings.Default.Utf8FilenameSupport) @@ -823,7 +985,7 @@ namespace APKToolGUI ToStatus(String.Format(Language.Aligning + " \"{0}\"...", Path.GetFileName(input)), Resources.waiting); })); - string tempApk = Path.Combine(Program.TEMP_PATH, "tempapk.apk"); + string tempApk = Path.Combine(Program.TempDirectory(), "tempapk.apk"); string outputApkFile = output; if (Settings.Default.Utf8FilenameSupport) @@ -870,7 +1032,7 @@ namespace APKToolGUI ToLog(ApktoolEventType.None, String.Format(Language.InputFile, input)); ToStatus(String.Format(Language.Signing + " \"{0}\"...", Path.GetFileName(input)), Resources.waiting); })); - string tempApk = Path.Combine(Program.TEMP_PATH, "tempapk.apk"); + string tempApk = Path.Combine(Program.TempDirectory(), "tempapk.apk"); string outputApkFile = output; if (Settings.Default.Utf8FilenameSupport) @@ -921,7 +1083,14 @@ namespace APKToolGUI private void openTempFolderToolStripMenuItem_Click(object sender, EventArgs e) { - Process.Start(Program.TEMP_PATH); + try + { + Process.Start(Program.TempDirectory()); + } + catch (Exception ex) + { + ToLog(ApktoolEventType.Error, ex.Message); + } } private void menuItemCheckUpdate_Click(object sender, EventArgs e) @@ -1058,9 +1227,10 @@ namespace APKToolGUI private void Application_ApplicationExit(object sender, EventArgs e) { - //Clear temp folder - DirectoryUtils.Delete(Program.TEMP_PATH); Save(); + + //Clear temp folder + DirectoryUtils.Delete(Program.TempDirectory()); } private bool ActionButtonsEnabled diff --git a/APKToolGUI/Forms/FormMain.resx b/APKToolGUI/Forms/FormMain.resx index fe97491..f26746f 100644 --- a/APKToolGUI/Forms/FormMain.resx +++ b/APKToolGUI/Forms/FormMain.resx @@ -835,13 +835,13 @@ 3, 3 - 50, 13 + 160, 13 12 - APK File: + APK/XAPK/APKS/ZIP/APKM File: label1 diff --git a/APKToolGUI/Forms/FormSettings.cs b/APKToolGUI/Forms/FormSettings.cs index 94b6c5a..913cc14 100644 --- a/APKToolGUI/Forms/FormSettings.cs +++ b/APKToolGUI/Forms/FormSettings.cs @@ -169,10 +169,10 @@ namespace APKToolGUI { customTempLocationTxtBox.Text = fbd.SelectedPath; //Clear temp folder - DirectoryUtils.Delete(Program.TEMP_PATH); + DirectoryUtils.Delete(Program.TempDirectory()); //Create new temp folder - Program.TEMP_PATH = Program.TempDir(); + Directory.CreateDirectory(Program.TempDirectory()); } } } diff --git a/APKToolGUI/Handlers/DecodeControlEventHandlers.cs b/APKToolGUI/Handlers/DecodeControlEventHandlers.cs index 966ee28..1021a30 100644 --- a/APKToolGUI/Handlers/DecodeControlEventHandlers.cs +++ b/APKToolGUI/Handlers/DecodeControlEventHandlers.cs @@ -70,7 +70,8 @@ namespace APKToolGUI.Handlers internal async void button_DECODE_Decode_Click(object sender, EventArgs e) { - if (File.Exists(main.textBox_DECODE_InputAppPath.Text)) + string inputFile = main.textBox_DECODE_InputAppPath.Text; + if (File.Exists(inputFile)) { if (main.checkBox_DECODE_UseFramework.Checked && !Directory.Exists(main.textBox_DECODE_FrameDir.Text)) { @@ -92,7 +93,10 @@ namespace APKToolGUI.Handlers } } - await main.Decompile(main.textBox_DECODE_InputAppPath.Text); + if (inputFile.ContainsAny(".xapk", ".zip", ".apks", ".apkm")) + await main.MergeAPK(inputFile); + else + await main.Decompile(inputFile); } else MessageBox.Show(Language.WarningFileForDecodingNotSelected, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error); @@ -107,7 +111,7 @@ namespace APKToolGUI.Handlers main.ToLog(ApktoolEventType.Error, Language.ErrorSelectedFileNotExist); } } - + internal void decOutOpenDirBtn_Click(object sender, EventArgs e) { if (Directory.Exists(Settings.Default.Decode_OutputDir)) diff --git a/APKToolGUI/Handlers/DragDropHandlers.cs b/APKToolGUI/Handlers/DragDropHandlers.cs index 57ca98d..d4db687 100644 --- a/APKToolGUI/Handlers/DragDropHandlers.cs +++ b/APKToolGUI/Handlers/DragDropHandlers.cs @@ -16,45 +16,48 @@ namespace APKToolGUI.Handlers class DragDropHandlers { private static FormMain main; + + string[] apks = { ".apk", ".xapk", ".zip", ".apks", ".apkm" }; + public DragDropHandlers(FormMain Main) { main = Main; //Decode DragEventHandler decEventHandler = new DragEventHandler((sender, e) => { DropApkToDec(e); }); - Register(main.decPanel, ".apk", decEventHandler); - Register(main.textBox_DECODE_InputAppPath, ".apk", decEventHandler, main.decPanel); - Register(main.button_DECODE_Decode, ".apk", decEventHandler, main.decPanel); + Register(main.decPanel, null, decEventHandler, apks); + Register(main.textBox_DECODE_InputAppPath, main.decPanel, decEventHandler, apks); + Register(main.button_DECODE_Decode, main.decPanel, decEventHandler, apks); DragEventHandler comEventHandler = new DragEventHandler((sender, e) => { DropDirToCom(e); }); - Register(main.comPanel, "", comEventHandler); - Register(main.textBox_BUILD_InputProjectDir, "", comEventHandler, main.comPanel); - Register(main.button_BUILD_Build, "", comEventHandler, main.comPanel); + Register(main.comPanel, null, comEventHandler, null); + Register(main.textBox_BUILD_InputProjectDir, main.comPanel, comEventHandler, null); + Register(main.button_BUILD_Build, main.comPanel, comEventHandler, null); DragEventHandler alignEventHandler = new DragEventHandler((sender, e) => { DropApkToAlign(e); }); - Register(main.zipalignPanel, ".apk", alignEventHandler); - Register(main.textBox_ZIPALIGN_InputFile, ".apk", alignEventHandler, main.zipalignPanel); - Register(main.button_ZIPALIGN_Align, ".apk", alignEventHandler, main.zipalignPanel); + Register(main.zipalignPanel, null, alignEventHandler, apks); + Register(main.textBox_ZIPALIGN_InputFile, main.zipalignPanel, alignEventHandler, apks); + Register(main.button_ZIPALIGN_Align, main.zipalignPanel, alignEventHandler, apks); DragEventHandler signEventHandler = new DragEventHandler((sender, e) => { DropApkToSign(e); }); - Register(main.signPanel, ".apk", signEventHandler); - Register(main.textBox_SIGN_InputFile, ".apk", signEventHandler, main.signPanel); - Register(main.button_SIGN_Sign, ".apk", signEventHandler, main.signPanel); + Register(main.signPanel, null, signEventHandler, apks); + Register(main.textBox_SIGN_InputFile, main.signPanel, signEventHandler, apks); + Register(main.button_SIGN_Sign, main.signPanel, signEventHandler, apks); DragEventHandler baksmaliEventHandler = new DragEventHandler((sender, e) => { DropDexToBaksmali(e); }); - Register(main.bakSmaliGroupBox, ".dex", baksmaliEventHandler); + Register(main.bakSmaliGroupBox, null, baksmaliEventHandler, new string[] { ".dex" }); main.bakSmaliGroupBox.AllowDrop = true; DragEventHandler smaliEventHandler = new DragEventHandler((sender, e) => { DropDirToSmali(e); }); - Register(main.smaliGroupBox, "", smaliEventHandler); + Register(main.smaliGroupBox, null, smaliEventHandler, null); main.smaliGroupBox.AllowDrop = true; DragEventHandler apkInfoEventHandler = new DragEventHandler((sender, e) => { DropApkToGetInfo(e); }); - Register(main.basicInfoTabPage, ".apk", apkInfoEventHandler); - Register(main.fileTxtBox, ".apk", apkInfoEventHandler); + Register(main.basicInfoTabPage, null, apkInfoEventHandler, apks); + Register(main.fileTxtBox, null, apkInfoEventHandler, apks); } - void Register(Control ctrl, string extension, DragEventHandler dragHandler, Control extCtrl = null) + void Register(Control ctrl, Control extCtrl, DragEventHandler dragHandler, string[] extension) { if (extCtrl == null) extCtrl = ctrl; @@ -67,20 +70,24 @@ namespace APKToolGUI.Handlers private async void DropApkToDec(DragEventArgs e) { string apkFile = null; - if (e.DropOneByEnd(".apk", file => apkFile = file)) + if (e.DropOneByEnd(file => apkFile = file, apks)) { - await main.GetApkInfo(apkFile); main.textBox_DECODE_InputAppPath.Text = apkFile; main.decPanel.BackColor = Color.White; - await main.Decompile(apkFile); + await main.GetApkInfo(apkFile); + + if (apkFile.ContainsAny(".xapk", ".zip", ".apks", ".apkm")) + await main.MergeAPK(apkFile); + else + await main.Decompile(apkFile); } } private async void DropDirToCom(DragEventArgs e) { string folder = null; - if (e.DropOneByEnd("", file => folder = file)) + if (e.DropOneByEnd(file => folder = file, null)) { if (File.Exists(Path.Combine(folder, "AndroidManifest.xml"))) { @@ -96,7 +103,7 @@ namespace APKToolGUI.Handlers private async void DropApkToAlign(DragEventArgs e) { string apkFile = null; - if (e.DropOneByEnd(".apk", file => apkFile = file)) + if (e.DropOneByEnd(file => apkFile = file, apks)) { main.textBox_ZIPALIGN_InputFile.Text = apkFile; main.zipalignPanel.BackColor = Color.White; @@ -131,7 +138,7 @@ namespace APKToolGUI.Handlers private async void DropApkToSign(DragEventArgs e) { string apkFile = null; - if (e.DropOneByEnd(".apk", file => apkFile = file)) + if (e.DropOneByEnd(file => apkFile = file, apks)) { main.textBox_SIGN_InputFile.Text = apkFile; main.signPanel.BackColor = Color.White; @@ -177,7 +184,7 @@ namespace APKToolGUI.Handlers private async void DropDexToBaksmali(DragEventArgs e) { string apkFile = null; - if (e.DropOneByEnd(".dex", file => apkFile = file)) + if (e.DropOneByEnd(file => apkFile = file, ".dex")) { main.baksmaliBrowseInputDexTxtBox.Text = apkFile; main.bakSmaliGroupBox.BackColor = Color.White; @@ -188,7 +195,7 @@ namespace APKToolGUI.Handlers private async void DropDirToSmali(DragEventArgs e) { string dir = null; - if (e.DropOneByEnd("", file => dir = file)) + if (e.DropOneByEnd(file => dir = file, null)) { main.smaliBrowseInputDirTxtBox.Text = dir; main.smaliGroupBox.BackColor = Color.White; @@ -199,7 +206,7 @@ namespace APKToolGUI.Handlers private void DropApkToGetInfo(DragEventArgs e) { string apkFile = null; - if (e.DropOneByEnd(".apk", file => apkFile = file)) + if (e.DropOneByEnd(file => apkFile = file, apks)) { main.smaliBrowseInputDirTxtBox.Text = apkFile; main.basicInfoTabPage.BackColor = Color.White; diff --git a/APKToolGUI/Languages/Language.Designer.cs b/APKToolGUI/Languages/Language.Designer.cs index 2659179..5d925d4 100644 --- a/APKToolGUI/Languages/Language.Designer.cs +++ b/APKToolGUI/Languages/Language.Designer.cs @@ -321,6 +321,15 @@ namespace APKToolGUI.Languages { } } + /// + /// Looks up a localized string similar to Decompiling all APK files. + /// + internal static string DecompilingAllApkFiles { + get { + return ResourceManager.GetString("DecompilingAllApkFiles", resourceCulture); + } + } + /// /// Looks up a localized string similar to Decompiling dex. /// @@ -348,6 +357,33 @@ namespace APKToolGUI.Languages { } } + /// + /// Looks up a localized string similar to {0} detected as base. + /// + internal static string DetectedAsBase { + get { + return ResourceManager.GetString("DetectedAsBase", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} detected as split. + /// + internal static string DetectedAsSplit { + get { + return ResourceManager.GetString("DetectedAsSplit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Directory {0} does not exist. + /// + internal static string DirNotExist { + get { + return ResourceManager.GetString("DirNotExist", resourceCulture); + } + } + /// /// Looks up a localized string similar to Done. /// @@ -582,6 +618,15 @@ namespace APKToolGUI.Languages { } } + /// + /// Looks up a localized string similar to Extracting all APK files. + /// + internal static string ExtractingAllApkFiles { + get { + return ResourceManager.GetString("ExtractingAllApkFiles", resourceCulture); + } + } + /// /// Looks up a localized string similar to File. /// @@ -726,6 +771,24 @@ namespace APKToolGUI.Languages { } } + /// + /// Looks up a localized string similar to Merge finished. Moving directory to {0}. + /// + internal static string MergeFinishedMoveDir { + get { + return ResourceManager.GetString("MergeFinishedMoveDir", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Merging APK. + /// + internal static string MergingApk { + get { + return ResourceManager.GetString("MergingApk", resourceCulture); + } + } + /// /// Looks up a localized string similar to META-INF folder does not exist. Skipped. /// @@ -753,6 +816,15 @@ namespace APKToolGUI.Languages { } } + /// + /// Looks up a localized string similar to Moving base directory to {0}. + /// + internal static string MovingBasedirectory { + get { + return ResourceManager.GetString("MovingBasedirectory", resourceCulture); + } + } + /// /// Looks up a localized string similar to For the changes to take effect you must restart the program. You want to do it now?. /// @@ -906,6 +978,15 @@ namespace APKToolGUI.Languages { } } + /// + /// Looks up a localized string similar to Temp directory: {0}. + /// + internal static string TempDirectory { + get { + return ResourceManager.GetString("TempDirectory", resourceCulture); + } + } + /// /// Looks up a localized string similar to Text file. /// diff --git a/APKToolGUI/Languages/Language.resx b/APKToolGUI/Languages/Language.resx index 7c981ca..8a50aab 100644 --- a/APKToolGUI/Languages/Language.resx +++ b/APKToolGUI/Languages/Language.resx @@ -432,4 +432,31 @@ The language is set. Do you want to restart the application? + + Decompiling all APK files + + + {0} detected as base + + + {0} detected as split + + + Extracting all APK files + + + Merge finished. Moving directory to {0} + + + Merging APK + + + Moving base directory to {0} + + + Directory {0} does not exist + + + Temp directory: {0} + \ No newline at end of file diff --git a/APKToolGUI/Program.cs b/APKToolGUI/Program.cs index 98cb86c..7f50911 100644 --- a/APKToolGUI/Program.cs +++ b/APKToolGUI/Program.cs @@ -62,7 +62,7 @@ namespace APKToolGUI } if (FilesCheck() == true) { - Directory.CreateDirectory(TEMP_PATH); + Directory.CreateDirectory(TempDirectory()); PortableSettingsProvider.SettingsFileName = "config.xml"; PortableSettingsProvider.ApplyProvider(Settings.Default); Application.Run(new FormMain()); @@ -165,18 +165,17 @@ namespace APKToolGUI return path; } - public static string TempDir() + public static string TempDirectory() { //Generate new every new instance to avoid conflict //We want to keep obfuscated path short as possible to prevent long path error if (Settings.Default.UseCustomTempDir) return Path.Combine(Settings.Default.TempDir, StringExt.RandStrWithCaps(5)); else - return Path.Combine(Path.GetTempPath(), StringExt.RandStrWithCaps(5)); + return Path.Combine(LOCAL_APPDATA_PATH, ASSEMBLY_NAME, StringExt.RandStrWithCaps(5)); } public static string LOCAL_APPDATA_PATH = Environment.GetEnvironmentVariable("LocalAppData"); - public static string TEMP_PATH = TempDir(); public static string APP_PATH = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); public static string RES_PATH = Path.Combine(APP_PATH, "Resources"); public static string ASSEMBLY_NAME = AssemblyName.GetAssemblyName(Assembly.GetExecutingAssembly().Location).Name; diff --git a/APKToolGUI/Utils/DirectoryUtils.cs b/APKToolGUI/Utils/DirectoryUtils.cs index ef8d47f..11c312f 100644 --- a/APKToolGUI/Utils/DirectoryUtils.cs +++ b/APKToolGUI/Utils/DirectoryUtils.cs @@ -52,19 +52,12 @@ namespace APKToolGUI.Utils Directory.CreateDirectory(targetFolder); foreach (var file in folder) { - //try - //{ - //Debug.WriteLine("Move file: " + file); var targetFile = Path.Combine(targetFolder, Path.GetFileName(file)); - File.Copy(file, targetFile, true); - //} - //catch (Exception ex) - //{ - // Debug.WriteLine("Error moving file: " + file + " (" + ex.Message + ")"); - //} + if (File.Exists(targetFile)) + File.Delete(targetFile); + File.Move(file, targetFile); } } - Directory.Delete(source, true); } public static void ReplaceinFiles( diff --git a/APKToolGUI/Utils/DragDropUtils.cs b/APKToolGUI/Utils/DragDropUtils.cs index f6e1482..b33c0c6 100644 --- a/APKToolGUI/Utils/DragDropUtils.cs +++ b/APKToolGUI/Utils/DragDropUtils.cs @@ -32,53 +32,27 @@ namespace SaveToGameWpf.Logic.Utils return filter == null ? items : items.Where(filter).ToArray(); } - public static void CheckDragEnter(this DragEventArgs e, string extensions) - { - if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return; - string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); - foreach (var file in files) - { - var ext = Path.GetExtension(file); - if (!String.IsNullOrEmpty(extensions) && ext.Equals(extensions)) - { - e.Effect = DragDropEffects.Copy; - return; - } - else if (String.IsNullOrEmpty(extensions)) - { - e.Effect = DragDropEffects.Copy; - return; - } - } - e.Effect = DragDropEffects.None; - } - public static bool CheckDragOver(this DragEventArgs e, string extensions) - { - if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return false; - string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); - foreach (var file in files) - { - var ext = Path.GetExtension(file); - if (!String.IsNullOrEmpty(extensions) && ext.Equals(extensions)) - { - e.Effect = DragDropEffects.Copy; - return true; - } - //else if (String.IsNullOrEmpty(extensions) && File.Exists(Path.Combine(file, "AndroidManifest.xml"))) - else if (String.IsNullOrEmpty(extensions)) - { - e.Effect = DragDropEffects.Copy; - return true; - } - } - return false; - } - - public static bool CheckManyDragOver(this DragEventArgs e, params string[] extensions) + public static void CheckDragEnter(this DragEventArgs e, params string[] extensions) { string[] files = e.GetFilesDrop(); - if (extensions.Any(ext => files[0].EndsWith(ext, StringComparison.Ordinal))) + if (extensions == null && Directory.Exists(files[0])) + e.Effect = DragDropEffects.Copy; + else if (extensions.Any(ext => files[0].EndsWith(ext, StringComparison.Ordinal))) + e.Effect = DragDropEffects.Copy; + else + e.Effect = DragDropEffects.None; + } + + public static bool CheckDragOver(this DragEventArgs e, params string[] extensions) + { + string[] files = e.GetFilesDrop(); + if (extensions == null && Directory.Exists(files[0])) + { + e.Effect = DragDropEffects.Move; + return true; + } + else if (files.Length == 1 && extensions.Any(ext => files[0].EndsWith(ext, StringComparison.Ordinal))) { e.Effect = DragDropEffects.Move; return true; @@ -88,46 +62,41 @@ namespace SaveToGameWpf.Logic.Utils return false; } - public static bool DropOneByEnd(this DragEventArgs e, string ext, Action onSuccess) - { - string[] files = e.GetFilesDrop(ext); - if (files.Length == 1) + public static bool CheckManyDragOver(this DragEventArgs e, params string[] extensions) + { + string[] files = e.GetFilesDrop(); + + if (extensions == null && Directory.Exists(files[0])) + { + e.Effect = DragDropEffects.Move; + return true; + } + else if (extensions.Any(ext => files[0].EndsWith(ext, StringComparison.Ordinal))) + { + e.Effect = DragDropEffects.Move; + return true; + } + e.Effect = DragDropEffects.None; + + return false; + } + + public static bool DropOneByEnd(this DragEventArgs e, Action onSuccess, params string[] extensions) + { + string[] files = e.GetFilesDrop(); + if (extensions == null && Directory.Exists(files[0])) + { + onSuccess(files[0]); + return true; + } + else if (extensions.Any(ext => files[0].EndsWith(ext, StringComparison.Ordinal))) { onSuccess(files[0]); - return true; } return false; } - - public static string DropOneByEnd(this DragEventArgs e, string ext) - { - string[] files = e.GetFilesDrop(ext); - - if (files.Length == 1) - return files[0]; - - return null; - } - - public static void DropManyByEnd(this DragEventArgs e, string ext, Action onSuccess) - { - string[] files = e.GetFilesDrop(ext); - - if (files.Length > 0) - onSuccess(files); - } - - public static string[] DropManyByEnd(this DragEventArgs e, string ext) - { - string[] files = e.GetFilesDrop(ext); - - if (files.Length > 0) - return files; - - return null; - } } } diff --git a/APKToolGUI/Utils/PathUtils.cs b/APKToolGUI/Utils/PathUtils.cs index 766b6a1..fe7440b 100644 --- a/APKToolGUI/Utils/PathUtils.cs +++ b/APKToolGUI/Utils/PathUtils.cs @@ -32,5 +32,16 @@ namespace APKToolGUI.Utils { return Path.Combine(Path.GetDirectoryName(path), Path.GetFileNameWithoutExtension(path)); } + + public static string GetRelativePath(string relativeTo, string path) + { + var uri = new Uri(relativeTo); + var rel = Uri.UnescapeDataString(uri.MakeRelativeUri(new Uri(path)).ToString()).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + if (rel.Contains(Path.DirectorySeparatorChar.ToString()) == false) + { + rel = $".{Path.DirectorySeparatorChar}{rel}"; + } + return rel; + } } } diff --git a/APKToolGUI/Utils/StringExt.cs b/APKToolGUI/Utils/StringExt.cs index a4583c1..46a24c2 100644 --- a/APKToolGUI/Utils/StringExt.cs +++ b/APKToolGUI/Utils/StringExt.cs @@ -69,5 +69,16 @@ namespace APKToolGUI.Utils return text; } } + + public static bool ContainsAny(this string haystack, params string[] needles) + { + foreach (string needle in needles) + { + if (haystack.Contains(needle)) + return true; + } + + return false; + } } } diff --git a/APKToolGUI/Utils/ZipUtils.cs b/APKToolGUI/Utils/ZipUtils.cs index ff2fe42..be39de2 100644 --- a/APKToolGUI/Utils/ZipUtils.cs +++ b/APKToolGUI/Utils/ZipUtils.cs @@ -112,6 +112,15 @@ namespace APKToolGUI.Utils } } + public static void ExtractAll(string path, string destination, bool flattenFoldersOnExtract = false) + { + using (ZipFile zip = ZipFile.Read(path)) + { + zip.FlattenFoldersOnExtract = flattenFoldersOnExtract; + zip.ExtractAll(destination, ExtractExistingFileAction.OverwriteSilently); + } + } + public static void AddDirectory(string path, string fileName, string directoryPathInArchive = "") { ZipFile zip = new ZipFile();