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*original*/string, /*replacement*/string> 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*original*/string, /*replacement*/string> 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();