Implement split apk merging. Use own temp directory

Apk merging is based on https://github.com/shadow578/ApksMerger
This commit is contained in:
AndnixSH
2023-03-02 12:28:48 +01:00
parent 8b4717c4b9
commit 1880b592f4
32 changed files with 1688 additions and 139 deletions
+18
View File
@@ -267,6 +267,24 @@
<Reference Include="WindowsFormsIntegration" />
</ItemGroup>
<ItemGroup>
<Compile Include="AndroidRes\AndroidResourceMerger.cs" />
<Compile Include="AndroidRes\AndroidResources.cs" />
<Compile Include="AndroidRes\Model\AndroidAttribute.cs" />
<Compile Include="AndroidRes\Model\AndroidBool.cs" />
<Compile Include="AndroidRes\Model\AndroidInteger.cs" />
<Compile Include="AndroidRes\Model\AndroidPlural.cs" />
<Compile Include="AndroidRes\Model\AndroidPublic.cs" />
<Compile Include="AndroidRes\Model\AndroidString.cs" />
<Compile Include="AndroidRes\Model\AndroidStyle.cs" />
<Compile Include="AndroidRes\Model\AndroidStyleable.cs" />
<Compile Include="AndroidRes\Model\AndroidTypedItem.cs" />
<Compile Include="AndroidRes\Model\GenericArrayTypes.cs" />
<Compile Include="AndroidRes\Model\GenericTypes.cs" />
<Compile Include="AndroidRes\Model\Generic\AndroidGeneric.cs" />
<Compile Include="AndroidRes\Model\Generic\AndroidGenericArray.cs" />
<Compile Include="AndroidRes\Model\Generic\AndroidResource.cs" />
<Compile Include="AndroidRes\Util\ClassExtensions.cs" />
<Compile Include="AndroidRes\Util\Log.cs" />
<Compile Include="ApkTool\AaptParser.cs" />
<Compile Include="ApkTool\ApkFixer.cs" />
<Compile Include="ApkTool\Apktool.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
{
/// <summary>
/// merges android resource files
/// </summary>
public sealed class AndroidMerger
{
/// <summary>
/// check capabilities of the base and splits, warn if (common) libs are missing
/// </summary>
/// <param name="locales">list of supported locales; key is locale, value is name of dir that first included it</param>
/// <param name="abis">list of supported abis; key is abi, value is name of dir that first included it</param>
/// <param name="baseDir">base project dir</param>
/// <param name="splits">split dirs</param>
public void CollectCapabilities(out Dictionary<string, string> locales, out Dictionary<string, string> abis,
DirectoryInfo baseDir, params DirectoryInfo[] splits)
{
//init dicts
Log.i("collecting info about splits...");
locales = new Dictionary<string, string>();
abis = new Dictionary<string, string>();
//combine base and splits into one list
List<DirectoryInfo> allDir = new List<DirectoryInfo>();
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>
//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");
}
}
}
/// <summary>
/// merge all splits into the base project dir
/// </summary>
/// <param name="baseDir">base project dir</param>
/// <param name="splits">split dirs to merge</param>
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<string> assetPacks = new List<string>();
//enumarate all splitted files
Dictionary</*original*/string, /*replacement*/string> globalNameReplacements = new Dictionary<string, string>();
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<string> 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);
}
/// <summary>
/// Patch the AndroidManifest.xml to not use splits
/// </summary>
/// <param name="manifest">the manifest xml to patch</param>
void PatchManifest(FileInfo manifest, List<string> 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<string> removeTargets = new List<string> { @"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("</application>") && !String.IsNullOrEmpty(modules))
{
oup.WriteLine(@" <meta-data android:name=""com.android.dynamic.apk.fused.modules"" android:value=""base" + modules + @"""/>");
}
//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<string> 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);
}
}
/// <summary>
/// merge two splitted resource xmls, overwrite a with merged
/// </summary>
/// <param name="a">file a to merge</param>
/// <param name="b">file b to merge</param>
/// <param name="globalNameReplacements">dictionary that can be used to replace names of resources globally</param>
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);
}
/// <summary>
/// check if the xml file contains the resources xml tag
/// </summary>
/// <param name="xml">the xml to check</param>
/// <returns>does the xml contain the tag?</returns>
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;
}
}
/// <summary>
/// should the file be processed?
/// Example for files to exclude from processing are AndroidManifest.xml, apktool.yml, and META-INF/*
/// </summary>
/// <param name="file">the file to check</param>
/// <param name="projDir">the project dir the file is in</param>
/// <returns>process the file?</returns>
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;
}
}
}
+103
View File
@@ -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<AndroidResource> Values { get; set; } = new List<AndroidResource>();
/// <summary>
/// Find a AndroidPublic with matching id
/// </summary>
/// <param name="id">the id to find</param>
/// <returns>matching public, or null if not found</returns>
public AndroidPublic FindPublicWithId(string id)
{
foreach(AndroidResource res in Values)
{
if((res is AndroidPublic pub) && pub.Id.Equals(id))
{
return pub;
}
}
return null;
}
/// <summary>
/// Deserialize a file into a object
/// </summary>
/// <param name="file">the file to deserialize</param>
/// <returns>the object</returns>
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;
}
}
/// <summary>
/// serialize into a file
/// </summary>
/// <param name="file">the file to serialize to, will be overwritten if exists</param>
/// <returns>write file ok?</returns>
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;
}
}
}
}
@@ -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; }
}
}
@@ -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; }
}
}
@@ -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; }
}
}
@@ -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<Plural> Values { get; set; } = new List<Plural>();
}
}
@@ -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; }
}
}
@@ -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; }
}
}
@@ -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<AndroidGeneric> Items { get; set; } = new List<AndroidGeneric>();
}
}
@@ -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<AndroidAttribute> Values { get; set; } = new List<AndroidAttribute>();
}
}
@@ -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; }
}
}
@@ -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; }
}
}
@@ -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<Item> Values { get; set; } = new List<Item>();
}
}
@@ -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);
}
}
}
@@ -0,0 +1,8 @@
using APKSMerger.AndroidRes.Model.Generic;
namespace APKSMerger.AndroidRes.Model
{
public sealed class AndroidStringArray : AndroidGenericArray { }
public sealed class AndroidIntegerArray : AndroidGenericArray { }
}
@@ -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 { }
}
@@ -0,0 +1,115 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace APKSMerger.Util
{
/// <summary>
/// extension methods
/// </summary>
public static class ClassExtensions
{
/// <summary>
/// replaces the first occurance of the pattern with the replacement
/// </summary>
/// <param name="s">the string to replace in</param>
/// <param name="pattern">the pattern to replace</param>
/// <param name="replacement">the replacement for the pattern</param>
/// <returns>a string in wich the first occurance of the pattern was replaced</returns>
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);
}
/// <summary>
/// does the array contain the string a, ignoring case?
/// </summary>
/// <param name="s">the array to check</param>
/// <param name="a">the string to check for</param>
/// <returns>contains it?</returns>
public static bool ContainsIgnoreCase(this string[] s, string a)
{
foreach (string sa in s)
{
if (sa.Equals(a, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
/// <summary>
/// enumerates all files in the directory (and subdirs if enabled)
/// </summary>
/// <param name="dir">the directory to enumerate in</param>
/// <param name="pattern">the pattern to filter with, eg. *.* or *.txt</param>
/// <param name="includeSubDirs">should files in subdirs be included?</param>
/// <param name="action">the action to execute for all files</param>
public static void EnumerateAllFiles(this DirectoryInfo dir, string pattern, bool includeSubDirs, Action<FileInfo> action)
{
foreach (FileInfo file in dir.EnumerateFiles(pattern, includeSubDirs ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
{
action.Invoke(file);
}
}
/// <summary>
/// enumerates all files in the directory (and subdirs if enabled) in parallel
/// </summary>
/// <param name="dir">the directory to enumerate in</param>
/// <param name="pattern">the pattern to filter with, eg. *.* or *.txt</param>
/// <param name="includeSubDirs">should files in subdirs be included?</param>
/// <param name="action">the action to execute for all files</param>
public static void EnumerateAllFilesParallel(this DirectoryInfo dir, string pattern, bool includeSubDirs, Action<FileInfo> action)
{
Parallel.ForEach(dir.EnumerateFiles(pattern, includeSubDirs ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly), action);
}
/// <summary>
/// checks if the two files have the same hash (MD5)
/// </summary>
/// <param name="a">the first file</param>
/// <param name="b">the file to compare</param>
/// <returns>do they have the same hash?</returns>
public static bool HasSameHash(this FileInfo a, FileInfo b)
{
return a.GetMD5().Equals(b.GetMD5());
}
/// <summary>
/// Get the md5 of the file
/// </summary>
/// <param name="f">the file to get md5 of</param>
/// <returns>md5 string of the file</returns>
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();
}
}
/// <summary>
/// repeat the char n times
/// </summary>
/// <param name="c">char to repeat</param>
/// <param name="n">how often to repeat</param>
/// <returns>string with n time c</returns>
public static string Repeat(this char c, int n)
{
string s = "";
for (int i = 0; i < n; i++)
s += c;
return s;
}
}
}
+265
View File
@@ -0,0 +1,265 @@
using APKToolGUI;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Media;
namespace APKSMerger.Util
{
/// <summary>
/// simple logging wrapper class
/// </summary>
public static class Log
{
/// <summary>
/// should very verbose logs (Log.vv) be written?
/// </summary>
public static bool LogVeryVerbose { get; set; } = false;
/// <summary>
/// should verbose logs (Log.v) be written?
/// </summary>
public static bool LogVerbose { get; set; } = true;
/// <summary>
/// should debug logs (Log.d) be written?
/// </summary>
public static bool LogDebug { get; set; } = true;
#region direct logs
/// <summary>
/// log message with level VERY VERBOSE (may be disabled)
/// </summary>
/// <param name="s">the string to log</param>
public static void vv(string s)
{
if (!LogVeryVerbose) return;
FormMain.Instance.ToLog(ApktoolEventType.None, s);
}
/// <summary>
/// log message with level VERBOSE (may be disabled)
/// </summary>
/// <param name="s">the string to log</param>
public static void v(string s)
{
if (!LogVerbose) return;
FormMain.Instance.ToLog(ApktoolEventType.None, s);
}
/// <summary>
/// log message with level DEBUG (may be disabled)
/// </summary>
/// <param name="s">the string to log</param>
public static void d(string s)
{
if (!LogDebug) return;
FormMain.Instance.ToLog(ApktoolEventType.None, s);
}
/// <summary>
/// log message with level INFO
/// </summary>
/// <param name="s">the string to log</param>
public static void i(string s)
{
FormMain.Instance.ToLog(ApktoolEventType.Infomation, s);
}
/// <summary>
/// log message with level WARNING
/// </summary>
/// <param name="s">the string to log</param>
public static void w(string s)
{
FormMain.Instance.ToLog(ApktoolEventType.Warning, s);
}
/// <summary>
/// log message with level ERROR
/// </summary>
/// <param name="s">the string to log</param>
public static void e(string s)
{
FormMain.Instance.ToLog(ApktoolEventType.Error, s);
}
#endregion
/// <summary>
/// start a new async log session
/// </summary>
/// <returns></returns>
public static AsyncLogSession StartAsync()
{
return new AsyncLogSession();
}
/// <summary>
/// writes a direct log message
/// </summary>
/// <param name="s">the string to log</param>
/// <param name="color">color to log in, null is default</param>
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;
}
}
/// <summary>
/// async log sesssion
/// </summary>
public class AsyncLogSession : IDisposable
{
/// <summary>
/// lock object to ensure only one object commits at a time
/// </summary>
static readonly object _CommitLock = new object();
/// <summary>
/// Tag to include when logging
/// </summary>
Stack<string> tags = new Stack<string>();
/// <summary>
/// contains all pending log entries
/// </summary>
Queue<string> pending = new Queue<string>();
#region log functions
/// <summary>
/// log message with level VERY VERBOSE (may be disabled)
/// </summary>
/// <param name="s">the string to log</param>
public void vv(string s)
{
if (!LogVeryVerbose) return;
EnqueueMessage($"[VV]{s}");
}
/// <summary>
/// log message with level VERBOSE (may be disabled)
/// </summary>
/// <param name="s">the string to log</param>
public void v(string s)
{
if (!LogVerbose) return;
EnqueueMessage($"[V]{s}");
}
/// <summary>
/// log message with level DEBUG (may be disabled)
/// </summary>
/// <param name="s">the string to log</param>
public void d(string s)
{
if (!LogDebug) return;
EnqueueMessage($"[D]{s}");
}
/// <summary>
/// log message with level INFO
/// </summary>
/// <param name="s">the string to log</param>
public void i(string s)
{
EnqueueMessage($"[I]{s}");
}
/// <summary>
/// log message with level WARNING
/// </summary>
/// <param name="s">the string to log</param>
public void w(string s)
{
EnqueueMessage($"[W]{s}");
}
/// <summary>
/// log message with level ERROR
/// </summary>
/// <param name="s">the string to log</param>
public void e(string s)
{
EnqueueMessage($"[E]{s}");
}
#endregion
/// <summary>
/// enqueue a message in the message queue
/// </summary>
/// <param name="s">the message to enqueue</param>
void EnqueueMessage(string s)
{
pending.Enqueue($"{GetTag()}:{s}");
}
/// <summary>
/// push a tag onto the tags stack
/// </summary>
/// <param name="t">the tag to push</param>
public void PushTag(string t)
{
tags.Push(t);
}
/// <summary>
/// pop the last tag of the tags stack
/// </summary>
public void PopTag()
{
// tags.TryPop(out _);
}
/// <summary>
/// get the current tag
/// </summary>
/// <returns>current tag, or string.Empty if no tag</returns>
public string GetTag()
{
string tag;
// if (!tags.TryPeek(out tag))
tag = string.Empty;
return tag;
}
/// <summary>
/// commit all pending log entries
/// </summary>
public void Commit()
{
lock (_CommitLock)
{
while (pending.Count > 0)
{
WriteLogDirect(pending.Dequeue());
}
}
}
/// <summary>
/// same as calling .Commit(). used for using() syntax
/// </summary>
public void Dispose()
{
Commit();
}
}
}
}
+185 -15
View File
@@ -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<int> 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<DirectoryInfo> splitDirs = new List<DirectoryInfo>();
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<string, string> 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
+2 -2
View File
@@ -835,13 +835,13 @@
<value>3, 3</value>
</data>
<data name="label1.Size" type="System.Drawing.Size, System.Drawing">
<value>50, 13</value>
<value>160, 13</value>
</data>
<data name="label1.TabIndex" type="System.Int32, mscorlib">
<value>12</value>
</data>
<data name="label1.Text" xml:space="preserve">
<value>APK File:</value>
<value>APK/XAPK/APKS/ZIP/APKM File:</value>
</data>
<data name="&gt;&gt;label1.Name" xml:space="preserve">
<value>label1</value>
+2 -2
View File
@@ -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());
}
}
}
@@ -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))
+33 -26
View File
@@ -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;
+81
View File
@@ -321,6 +321,15 @@ namespace APKToolGUI.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Decompiling all APK files.
/// </summary>
internal static string DecompilingAllApkFiles {
get {
return ResourceManager.GetString("DecompilingAllApkFiles", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Decompiling dex.
/// </summary>
@@ -348,6 +357,33 @@ namespace APKToolGUI.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to {0} detected as base.
/// </summary>
internal static string DetectedAsBase {
get {
return ResourceManager.GetString("DetectedAsBase", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} detected as split.
/// </summary>
internal static string DetectedAsSplit {
get {
return ResourceManager.GetString("DetectedAsSplit", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Directory {0} does not exist.
/// </summary>
internal static string DirNotExist {
get {
return ResourceManager.GetString("DirNotExist", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Done.
/// </summary>
@@ -582,6 +618,15 @@ namespace APKToolGUI.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Extracting all APK files.
/// </summary>
internal static string ExtractingAllApkFiles {
get {
return ResourceManager.GetString("ExtractingAllApkFiles", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to File.
/// </summary>
@@ -726,6 +771,24 @@ namespace APKToolGUI.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Merge finished. Moving directory to {0}.
/// </summary>
internal static string MergeFinishedMoveDir {
get {
return ResourceManager.GetString("MergeFinishedMoveDir", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Merging APK.
/// </summary>
internal static string MergingApk {
get {
return ResourceManager.GetString("MergingApk", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to META-INF folder does not exist. Skipped.
/// </summary>
@@ -753,6 +816,15 @@ namespace APKToolGUI.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Moving base directory to {0}.
/// </summary>
internal static string MovingBasedirectory {
get {
return ResourceManager.GetString("MovingBasedirectory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to For the changes to take effect you must restart the program. You want to do it now?.
/// </summary>
@@ -906,6 +978,15 @@ namespace APKToolGUI.Languages {
}
}
/// <summary>
/// Looks up a localized string similar to Temp directory: {0}.
/// </summary>
internal static string TempDirectory {
get {
return ResourceManager.GetString("TempDirectory", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Text file.
/// </summary>
+27
View File
@@ -432,4 +432,31 @@
<data name="SetLanguageRestartApplication" xml:space="preserve">
<value>The language is set. Do you want to restart the application?</value>
</data>
<data name="DecompilingAllApkFiles" xml:space="preserve">
<value>Decompiling all APK files</value>
</data>
<data name="DetectedAsBase" xml:space="preserve">
<value>{0} detected as base</value>
</data>
<data name="DetectedAsSplit" xml:space="preserve">
<value>{0} detected as split</value>
</data>
<data name="ExtractingAllApkFiles" xml:space="preserve">
<value>Extracting all APK files</value>
</data>
<data name="MergeFinishedMoveDir" xml:space="preserve">
<value>Merge finished. Moving directory to {0}</value>
</data>
<data name="MergingApk" xml:space="preserve">
<value>Merging APK</value>
</data>
<data name="MovingBasedirectory" xml:space="preserve">
<value>Moving base directory to {0}</value>
</data>
<data name="DirNotExist" xml:space="preserve">
<value>Directory {0} does not exist</value>
</data>
<data name="TempDirectory" xml:space="preserve">
<value>Temp directory: {0}</value>
</data>
</root>
+3 -4
View File
@@ -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;
+3 -10
View File
@@ -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(
+46 -77
View File
@@ -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<string> 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<string> 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<string[]> 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;
}
}
}
+11
View File
@@ -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;
}
}
}
+11
View File
@@ -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;
}
}
}
+9
View File
@@ -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();