ref: split ApkManifest into data parser and content
This commit is contained in:
@@ -11,4 +11,4 @@ MACOSX_DEPLOYMENT_TARGET = 10.15
|
|||||||
MARKETING_VERSION = 1.4.0
|
MARKETING_VERSION = 1.4.0
|
||||||
PRODUCT_NAME = QLAppBundle
|
PRODUCT_NAME = QLAppBundle
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLAppBundle
|
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.QLAppBundle
|
||||||
CURRENT_PROJECT_VERSION = 1981
|
CURRENT_PROJECT_VERSION = 2015
|
||||||
|
|||||||
@@ -9,12 +9,15 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF5D2EA1199B00613856 /* MetaInfo.swift */; };
|
5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF5D2EA1199B00613856 /* MetaInfo.swift */; };
|
||||||
5405CF652EA1376B00613856 /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF642EA1376B00613856 /* Zip.swift */; };
|
5405CF652EA1376B00613856 /* Zip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5405CF642EA1376B00613856 /* Zip.swift */; };
|
||||||
540B77D92ED79BBD009E030C /* ApkManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540B77D82ED79BB2009E030C /* ApkManifest.swift */; };
|
540B77D92ED79BBD009E030C /* Apk+Manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540B77D82ED79BB2009E030C /* Apk+Manifest.swift */; };
|
||||||
540B77DA2ED79C6B009E030C /* ApkManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 540B77D82ED79BB2009E030C /* ApkManifest.swift */; };
|
|
||||||
540B77DC2ED79CC1009E030C /* AndroidXML in Frameworks */ = {isa = PBXBuildFile; productRef = 540B77DB2ED79CC1009E030C /* AndroidXML */; };
|
540B77DC2ED79CC1009E030C /* AndroidXML in Frameworks */ = {isa = PBXBuildFile; productRef = 540B77DB2ED79CC1009E030C /* AndroidXML */; };
|
||||||
540B77DE2ED79CC8009E030C /* AndroidXML in Frameworks */ = {isa = PBXBuildFile; productRef = 540B77DD2ED79CC8009E030C /* AndroidXML */; };
|
540B77DE2ED79CC8009E030C /* AndroidXML in Frameworks */ = {isa = PBXBuildFile; productRef = 540B77DD2ED79CC8009E030C /* AndroidXML */; };
|
||||||
5412DECE2EBC168600F9040D /* Preview+ArchiveInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412DECD2EBC168600F9040D /* Preview+ArchiveInfo.swift */; };
|
5412DECE2EBC168600F9040D /* Preview+ArchiveInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412DECD2EBC168600F9040D /* Preview+ArchiveInfo.swift */; };
|
||||||
5412DED02EBC283000F9040D /* RuntimeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412DECF2EBC283000F9040D /* RuntimeError.swift */; };
|
5412DED02EBC283000F9040D /* RuntimeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412DECF2EBC283000F9040D /* RuntimeError.swift */; };
|
||||||
|
543899082EDCA223007C02FC /* Apk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543899072EDCA223007C02FC /* Apk.swift */; };
|
||||||
|
543899092EDCA26D007C02FC /* Apk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543899072EDCA223007C02FC /* Apk.swift */; };
|
||||||
|
5438990B2EDCA27F007C02FC /* Apk+Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5438990A2EDCA27F007C02FC /* Apk+Icon.swift */; };
|
||||||
|
5438990E2EDCB126007C02FC /* Apk+Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5438990A2EDCA27F007C02FC /* Apk+Icon.swift */; };
|
||||||
543FE5742EB3BB5E0059F98B /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 543FE5732EB3BB5E0059F98B /* AppIcon.icns */; };
|
543FE5742EB3BB5E0059F98B /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 543FE5732EB3BB5E0059F98B /* AppIcon.icns */; };
|
||||||
54442C232E378BAF008A870E /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54442C222E378BAF008A870E /* Quartz.framework */; };
|
54442C232E378BAF008A870E /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54442C222E378BAF008A870E /* Quartz.framework */; };
|
||||||
54442C702E378BDD008A870E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54442C6A2E378BDD008A870E /* AppDelegate.swift */; };
|
54442C702E378BDD008A870E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54442C6A2E378BDD008A870E /* AppDelegate.swift */; };
|
||||||
@@ -137,10 +140,12 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
5405CF5D2EA1199B00613856 /* MetaInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaInfo.swift; sourceTree = "<group>"; };
|
5405CF5D2EA1199B00613856 /* MetaInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaInfo.swift; sourceTree = "<group>"; };
|
||||||
5405CF642EA1376B00613856 /* Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zip.swift; sourceTree = "<group>"; };
|
5405CF642EA1376B00613856 /* Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zip.swift; sourceTree = "<group>"; };
|
||||||
540B77D82ED79BB2009E030C /* ApkManifest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApkManifest.swift; sourceTree = "<group>"; };
|
540B77D82ED79BB2009E030C /* Apk+Manifest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Apk+Manifest.swift"; sourceTree = "<group>"; };
|
||||||
5412DECD2EBC168600F9040D /* Preview+ArchiveInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preview+ArchiveInfo.swift"; sourceTree = "<group>"; };
|
5412DECD2EBC168600F9040D /* Preview+ArchiveInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Preview+ArchiveInfo.swift"; sourceTree = "<group>"; };
|
||||||
5412DECF2EBC283000F9040D /* RuntimeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeError.swift; sourceTree = "<group>"; };
|
5412DECF2EBC283000F9040D /* RuntimeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeError.swift; sourceTree = "<group>"; };
|
||||||
54352E8A2EB6A79A0082F61D /* AssetCarReader.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AssetCarReader.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
54352E8A2EB6A79A0082F61D /* AssetCarReader.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AssetCarReader.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
543899072EDCA223007C02FC /* Apk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Apk.swift; sourceTree = "<group>"; };
|
||||||
|
5438990A2EDCA27F007C02FC /* Apk+Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Apk+Icon.swift"; sourceTree = "<group>"; };
|
||||||
543FE5732EB3BB5E0059F98B /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = "<group>"; };
|
543FE5732EB3BB5E0059F98B /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = "<group>"; };
|
||||||
543FE5752EB3BC740059F98B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
543FE5752EB3BC740059F98B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
54442BF42E378B71008A870E /* QLAppBundle (debug).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "QLAppBundle (debug).app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
54442BF42E378B71008A870E /* QLAppBundle (debug).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "QLAppBundle (debug).app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -245,7 +250,9 @@
|
|||||||
5469E11C2EA5930C00D46CE7 /* Entitlements.swift */,
|
5469E11C2EA5930C00D46CE7 /* Entitlements.swift */,
|
||||||
54D3A6EB2EA31B52001EF4F6 /* AppCategories.swift */,
|
54D3A6EB2EA31B52001EF4F6 /* AppCategories.swift */,
|
||||||
54CCF59D2EDC9A6800D766F9 /* AndroidSdkMap.swift */,
|
54CCF59D2EDC9A6800D766F9 /* AndroidSdkMap.swift */,
|
||||||
540B77D82ED79BB2009E030C /* ApkManifest.swift */,
|
543899072EDCA223007C02FC /* Apk.swift */,
|
||||||
|
5438990A2EDCA27F007C02FC /* Apk+Icon.swift */,
|
||||||
|
540B77D82ED79BB2009E030C /* Apk+Manifest.swift */,
|
||||||
547F52DC2EB2C15D002B6D5F /* ExpirationStatus.swift */,
|
547F52DC2EB2C15D002B6D5F /* ExpirationStatus.swift */,
|
||||||
547F52E62EB2C41C002B6D5F /* PreviewGenerator.swift */,
|
547F52E62EB2C41C002B6D5F /* PreviewGenerator.swift */,
|
||||||
547F52EC2EB2C822002B6D5F /* Preview+AppInfo.swift */,
|
547F52EC2EB2C822002B6D5F /* Preview+AppInfo.swift */,
|
||||||
@@ -566,6 +573,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
543899082EDCA223007C02FC /* Apk.swift in Sources */,
|
||||||
549E3BA42EBC021500ADFF56 /* Preview+TransportSecurity.swift in Sources */,
|
549E3BA42EBC021500ADFF56 /* Preview+TransportSecurity.swift in Sources */,
|
||||||
5412DECE2EBC168600F9040D /* Preview+ArchiveInfo.swift in Sources */,
|
5412DECE2EBC168600F9040D /* Preview+ArchiveInfo.swift in Sources */,
|
||||||
54D3A6F02EA3F49F001EF4F6 /* NSBezierPath+RoundedRect.swift in Sources */,
|
54D3A6F02EA3F49F001EF4F6 /* NSBezierPath+RoundedRect.swift in Sources */,
|
||||||
@@ -581,12 +589,13 @@
|
|||||||
54993B952EDBC819008B656D /* Plist+MobileProvision.swift in Sources */,
|
54993B952EDBC819008B656D /* Plist+MobileProvision.swift in Sources */,
|
||||||
547F52E82EB2C41C002B6D5F /* PreviewGenerator.swift in Sources */,
|
547F52E82EB2C41C002B6D5F /* PreviewGenerator.swift in Sources */,
|
||||||
54993B882EDB9AA1008B656D /* Plist+Info.swift in Sources */,
|
54993B882EDB9AA1008B656D /* Plist+Info.swift in Sources */,
|
||||||
|
5438990B2EDCA27F007C02FC /* Apk+Icon.swift in Sources */,
|
||||||
547F52EB2EB2C672002B6D5F /* Preview+FileInfo.swift in Sources */,
|
547F52EB2EB2C672002B6D5F /* Preview+FileInfo.swift in Sources */,
|
||||||
54CCF59E2EDC9A6800D766F9 /* AndroidSdkMap.swift in Sources */,
|
54CCF59E2EDC9A6800D766F9 /* AndroidSdkMap.swift in Sources */,
|
||||||
547F52ED2EB2C822002B6D5F /* Preview+AppInfo.swift in Sources */,
|
547F52ED2EB2C822002B6D5F /* Preview+AppInfo.swift in Sources */,
|
||||||
549E3BA12EBAE7D300ADFF56 /* URL+File.swift in Sources */,
|
549E3BA12EBAE7D300ADFF56 /* URL+File.swift in Sources */,
|
||||||
547F52F42EB2CA05002B6D5F /* Preview+Entitlements.swift in Sources */,
|
547F52F42EB2CA05002B6D5F /* Preview+Entitlements.swift in Sources */,
|
||||||
540B77D92ED79BBD009E030C /* ApkManifest.swift in Sources */,
|
540B77D92ED79BBD009E030C /* Apk+Manifest.swift in Sources */,
|
||||||
5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */,
|
5405CF5E2EA1199B00613856 /* MetaInfo.swift in Sources */,
|
||||||
547F52F92EB2CBAB002B6D5F /* Date+Format.swift in Sources */,
|
547F52F92EB2CBAB002B6D5F /* Date+Format.swift in Sources */,
|
||||||
54D3A6EC2EA31B52001EF4F6 /* AppCategories.swift in Sources */,
|
54D3A6EC2EA31B52001EF4F6 /* AppCategories.swift in Sources */,
|
||||||
@@ -604,10 +613,11 @@
|
|||||||
547899722EB38F3D00F96B80 /* MetaInfo.swift in Sources */,
|
547899722EB38F3D00F96B80 /* MetaInfo.swift in Sources */,
|
||||||
547899732EB38F3D00F96B80 /* NSBezierPath+RoundedRect.swift in Sources */,
|
547899732EB38F3D00F96B80 /* NSBezierPath+RoundedRect.swift in Sources */,
|
||||||
54AE5BFF2EB3DB1000B4CFC7 /* ThumbnailProvider.swift in Sources */,
|
54AE5BFF2EB3DB1000B4CFC7 /* ThumbnailProvider.swift in Sources */,
|
||||||
|
543899092EDCA26D007C02FC /* Apk.swift in Sources */,
|
||||||
549E3BA22EBAECD400ADFF56 /* URL+File.swift in Sources */,
|
549E3BA22EBAECD400ADFF56 /* URL+File.swift in Sources */,
|
||||||
547899752EB38F3D00F96B80 /* AppIcon.swift in Sources */,
|
547899752EB38F3D00F96B80 /* AppIcon.swift in Sources */,
|
||||||
540B77DA2ED79C6B009E030C /* ApkManifest.swift in Sources */,
|
|
||||||
54993B8B2EDBA75A008B656D /* Plist+Icon.swift in Sources */,
|
54993B8B2EDBA75A008B656D /* Plist+Icon.swift in Sources */,
|
||||||
|
5438990E2EDCB126007C02FC /* Apk+Icon.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
74
src/Apk+Icon.swift
Normal file
74
src/Apk+Icon.swift
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import Foundation
|
||||||
|
import AndroidXML
|
||||||
|
|
||||||
|
extension MetaInfo {
|
||||||
|
/// Read `AndroidManifest.xml` but only extract `appIcon`.
|
||||||
|
func readApk_Icon() -> Apk_Icon? {
|
||||||
|
assert(type == .APK)
|
||||||
|
var apk = Apk(self)
|
||||||
|
return Apk_Icon(&apk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Apk_Icon
|
||||||
|
|
||||||
|
/// Representation of `AndroidManifest.xml` (containing only the icon extractor).
|
||||||
|
/// Seperate from main class because everything else is not needed for `ThumbnailProvider`
|
||||||
|
struct Apk_Icon {
|
||||||
|
let path: String
|
||||||
|
let data: Data
|
||||||
|
|
||||||
|
init?(_ apk: inout Apk, iconRef: String? = nil) {
|
||||||
|
if apk.isApkm, let iconData = apk.mainZip.unzipFile("icon.png") {
|
||||||
|
path = "icon.png"
|
||||||
|
data = iconData
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let manifest = apk.manifest else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ref = iconRef
|
||||||
|
// no need to parse xml if reference already supplied
|
||||||
|
if ref == nil {
|
||||||
|
if let xml = try? AndroidXML.init(data: manifest) {
|
||||||
|
let parser = xml.parseXml()
|
||||||
|
try? parser.iterElements({ startTag, attributes in
|
||||||
|
if startTag == "application" {
|
||||||
|
ref = try? attributes.get("android:icon")?.resolve(parser.stringPool)
|
||||||
|
}
|
||||||
|
}) {_ in}
|
||||||
|
} else {
|
||||||
|
// fallback to xml-string parser
|
||||||
|
ref = ApkXmlIconParser().run(manifest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let img = apk.resolveIcon(&ref) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
path = ref!
|
||||||
|
data = img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorter form of `ApkXmlManifestParser` to only exctract the icon reference (used for Thumbnail Provider)
|
||||||
|
private class ApkXmlIconParser: NSObject, XMLParserDelegate {
|
||||||
|
var result: String? = nil
|
||||||
|
|
||||||
|
func run(_ data: Data) -> String? {
|
||||||
|
let parser = XMLParser(data: data)
|
||||||
|
parser.delegate = self
|
||||||
|
parser.parse()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attrs: [String : String] = [:]) {
|
||||||
|
if elementName == "application" {
|
||||||
|
result = attrs["android:icon"]
|
||||||
|
parser.abortParsing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/Apk+Manifest.swift
Normal file
123
src/Apk+Manifest.swift
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import Foundation
|
||||||
|
import AndroidXML
|
||||||
|
|
||||||
|
extension MetaInfo {
|
||||||
|
/// Read `AndroidManifest.xml` and parse its content
|
||||||
|
func readApk_Manifest() -> Apk_Manifest? {
|
||||||
|
assert(type == .APK)
|
||||||
|
var apk = Apk(self)
|
||||||
|
return Apk_Manifest.from(&apk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Apk_Manifest
|
||||||
|
|
||||||
|
/// Representation of `AndroidManifest.xml`.
|
||||||
|
/// See: <https://developer.android.com/guide/topics/manifest/manifest-element>
|
||||||
|
struct Apk_Manifest {
|
||||||
|
var packageId: String? = nil
|
||||||
|
var appName: String? = nil
|
||||||
|
var icon: Apk_Icon? = nil
|
||||||
|
/// Computed property
|
||||||
|
var appIconData: Data? = nil
|
||||||
|
var versionName: String? = nil
|
||||||
|
var versionCode: String? = nil
|
||||||
|
var sdkVerMin: Int? = nil
|
||||||
|
var sdkVerTarget: Int? = nil
|
||||||
|
|
||||||
|
var featuresRequired: [String] = []
|
||||||
|
var featuresOptional: [String] = []
|
||||||
|
var permissions: [String] = []
|
||||||
|
|
||||||
|
static func from(_ apk: inout Apk) -> Self? {
|
||||||
|
guard let manifest = apk.manifest else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let storage = ApkXmlManifestParser()
|
||||||
|
if let xml = try? AndroidXML.init(data: manifest) {
|
||||||
|
let parser = xml.parseXml()
|
||||||
|
let ignore = XMLParser()
|
||||||
|
try? parser.iterElements({ startTag, attributes in
|
||||||
|
if ALLOWED_TAGS.contains(startTag) {
|
||||||
|
storage.parser(ignore, didStartElement: startTag, namespaceURI: nil, qualifiedName: nil, attributes: try attributes.asDictStr())
|
||||||
|
}
|
||||||
|
}) { endTag in
|
||||||
|
if ALLOWED_TAGS.contains(endTag) {
|
||||||
|
storage.parser(ignore, didEndElement: endTag, namespaceURI: nil, qualifiedName: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// fallback to xml-string parser
|
||||||
|
let parser = XMLParser(data: manifest)
|
||||||
|
parser.delegate = storage
|
||||||
|
parser.parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
var rv = storage.result
|
||||||
|
apk.resolveName(&rv.appName)
|
||||||
|
rv.icon = Apk_Icon(&apk, iconRef: storage.iconRef)
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep in sync with `ApkXmlManifestParser` below
|
||||||
|
private let ALLOWED_TAGS = [
|
||||||
|
"manifest",
|
||||||
|
"application",
|
||||||
|
"uses-feature",
|
||||||
|
"uses-permission",
|
||||||
|
"uses-permission-sdk-23",
|
||||||
|
"uses-sdk",
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Wrapper to use same code for binary-xml and string-xml parsing
|
||||||
|
private class ApkXmlManifestParser: NSObject, XMLParserDelegate {
|
||||||
|
private var _scope: [String] = []
|
||||||
|
var result = Apk_Manifest()
|
||||||
|
var iconRef: String? = nil
|
||||||
|
|
||||||
|
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attrs: [String : String] = [:]) {
|
||||||
|
// keep in sync with `ALLOWED_TAGS` above
|
||||||
|
switch elementName {
|
||||||
|
case "manifest":
|
||||||
|
if _scope == [] {
|
||||||
|
result.packageId = attrs["package"] // "org.bundle.id"
|
||||||
|
result.versionName = attrs["android:versionName"] // "7.62.3"
|
||||||
|
result.versionCode = attrs["android:versionCode"] // "160700"
|
||||||
|
// attrs["platformBuildVersionCode"] // "35"
|
||||||
|
// attrs["platformBuildVersionName"] // "15"
|
||||||
|
}
|
||||||
|
case "application":
|
||||||
|
if _scope == ["manifest"] {
|
||||||
|
result.appName = attrs["android:label"] // @resource-ref
|
||||||
|
iconRef = attrs["android:icon"] // @resource-ref
|
||||||
|
}
|
||||||
|
case "uses-permission", "uses-permission-sdk-23":
|
||||||
|
// no "permission" because that will produce duplicates with "uses-permission"
|
||||||
|
if _scope == ["manifest"], let name = attrs["android:name"] {
|
||||||
|
result.permissions.append(name)
|
||||||
|
}
|
||||||
|
case "uses-feature":
|
||||||
|
if _scope == ["manifest"], let name = attrs["android:name"] {
|
||||||
|
if attrs["android:required"] == "false" {
|
||||||
|
result.featuresOptional.append(name)
|
||||||
|
} else {
|
||||||
|
result.featuresRequired.append(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "uses-sdk":
|
||||||
|
if _scope == ["manifest"] {
|
||||||
|
result.sdkVerMin = Int(attrs["android:minSdkVersion"] ?? "1") // "21"
|
||||||
|
result.sdkVerTarget = Int(attrs["android:targetSdkVersion"] ?? "-1") // "35"
|
||||||
|
}
|
||||||
|
default: break // ignore
|
||||||
|
}
|
||||||
|
_scope.append(elementName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
|
||||||
|
_scope.removeLast()
|
||||||
|
}
|
||||||
|
}
|
||||||
130
src/Apk.swift
Normal file
130
src/Apk.swift
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import Foundation
|
||||||
|
import AndroidXML
|
||||||
|
|
||||||
|
/// Data structure for processing the content of `.apk` files.
|
||||||
|
struct Apk {
|
||||||
|
let isApkm: Bool
|
||||||
|
let mainZip: ZipFile
|
||||||
|
|
||||||
|
init(_ meta: MetaInfo) {
|
||||||
|
isApkm = meta.url.pathExtension.lowercased() == "apkm"
|
||||||
|
mainZip = meta.zipFile!
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unzip `AndroidManifest.xml` (once). Data is cached until deconstructor.
|
||||||
|
lazy var manifest: Data? = { effectiveZip?.unzipFile("AndroidManifest.xml") }()
|
||||||
|
|
||||||
|
/// Select zip-file depending on `.apk` or `.apkm` extension
|
||||||
|
private lazy var effectiveZip: ZipFile? = { isApkm ? nestedZip : mainZip }()
|
||||||
|
|
||||||
|
/// `.apkm` may contain multiple `.apk` files. (plus "icon.png" and "info.json" files)
|
||||||
|
private lazy var nestedZip: ZipFile? = {
|
||||||
|
if isApkm, let pth = try? mainZip.unzipFileToTempDir("base.apk") {
|
||||||
|
return ZipFile(pth)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
/// Shared instance for resolving resources
|
||||||
|
private lazy var resourceParser: Tbl_Parser? = {
|
||||||
|
guard let data = effectiveZip?.unzipFile("resources.arsc"),
|
||||||
|
let xml = try? AndroidXML.init(data: data), xml.type == .Table else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return xml.parseTable()
|
||||||
|
}()
|
||||||
|
|
||||||
|
/// Lookup app bundle name / label
|
||||||
|
mutating func resolveName(_ name: inout String?) {
|
||||||
|
if let val = name, let ref = try? TblTableRef(val), let parser = resourceParser {
|
||||||
|
name = parser.getName(ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lookup image path and image data
|
||||||
|
mutating func resolveIcon(_ iconRef: inout String?) -> Data? {
|
||||||
|
if let val = iconRef, let ref = try? TblTableRef(val), let parser = resourceParser {
|
||||||
|
if let iconPath = parser.getIconDirect(ref) ?? parser.getIconIndirect(ref) {
|
||||||
|
iconRef = iconPath
|
||||||
|
return effectiveZip?.unzipFile(iconPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private extension Tbl_Parser {
|
||||||
|
func getName(_ ref: TblTableRef) -> String? {
|
||||||
|
// why the heck are these even allowed?
|
||||||
|
// apparently there can be references onto references
|
||||||
|
var ref = ref
|
||||||
|
while let res = try? self.getResource(ref) {
|
||||||
|
guard let val = res.entries.first?.entry.value else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch val.dataType {
|
||||||
|
case .Reference: ref = val.asTableRef // and continue
|
||||||
|
case .String: return val.resolve(self.stringPool)
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lookup resource with matching id. Choose the icon with the highest density.
|
||||||
|
func getIconDirect(_ ref: TblTableRef) -> String? {
|
||||||
|
guard let res = try? self.getResource(ref) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var best: ResValue? = nil
|
||||||
|
var bestScore: UInt16 = 0
|
||||||
|
for e in res.entries {
|
||||||
|
switch e.config.screenType.density {
|
||||||
|
case .Default, .any, .None: continue
|
||||||
|
case let density:
|
||||||
|
if density.rawValue > bestScore, let val = e.entry.value {
|
||||||
|
bestScore = density.rawValue
|
||||||
|
best = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best?.resolve(self.stringPool)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all entries and choose best-rated icon file.
|
||||||
|
/// Rating prefers files which have an attribute name `"app_icon"` or `"ic_launcher"`.
|
||||||
|
func getIconIndirect(_ ref: TblTableRef) -> String? {
|
||||||
|
// sadly we cannot just `getResource()` because that can point to an app banner
|
||||||
|
guard let pkg = try? self.getPackage(ref.package),
|
||||||
|
var pool = pkg.stringPool(for: .Keys),
|
||||||
|
let (_, types) = try? pkg.getType(ref.type) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// density is 120-640
|
||||||
|
let rates: [String: UInt16] = [
|
||||||
|
"app_icon": 1000,
|
||||||
|
"ic_launcher": 800,
|
||||||
|
"ic_launcher_foreground": 200,
|
||||||
|
]
|
||||||
|
var best: ResValue? = nil
|
||||||
|
var bestScore: UInt16 = 0
|
||||||
|
for typ in types {
|
||||||
|
switch typ.config.screenType.density {
|
||||||
|
case .any, .None: continue
|
||||||
|
case let density:
|
||||||
|
try? typ.iterValues {
|
||||||
|
if let val = $1.value {
|
||||||
|
let attrName = pool.getStringCached($1.key)
|
||||||
|
let score = density.rawValue + (rates[attrName] ?? 0)
|
||||||
|
if score > bestScore {
|
||||||
|
bestScore = score
|
||||||
|
best = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best?.resolve(self.stringPool)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import AndroidXML
|
|
||||||
import os // OSLog
|
|
||||||
|
|
||||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ApkManifest")
|
|
||||||
|
|
||||||
/// Representation of `AndroidManifest.xml`.
|
|
||||||
/// See: <https://developer.android.com/guide/topics/manifest/manifest-element>
|
|
||||||
struct ApkManifest {
|
|
||||||
var packageId: String? = nil
|
|
||||||
var appName: String? = nil
|
|
||||||
var appIcon: String? = nil
|
|
||||||
/// Computed property
|
|
||||||
var appIconData: Data? = nil
|
|
||||||
var versionName: String? = nil
|
|
||||||
var versionCode: String? = nil
|
|
||||||
var sdkVerMin: Int? = nil
|
|
||||||
var sdkVerTarget: Int? = nil
|
|
||||||
|
|
||||||
var featuresRequired: [String] = []
|
|
||||||
var featuresOptional: [String] = []
|
|
||||||
var permissions: [String] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Full Manifest
|
|
||||||
|
|
||||||
extension MetaInfo {
|
|
||||||
/// `(true, <nested-apk>)` -- if extension is `.apkm`.
|
|
||||||
/// `(false, zipFile!)` -- if extension is `.apk`.
|
|
||||||
private func effectiveApk() -> (Bool, ZipFile) {
|
|
||||||
// .apkm may contain multiple .apk files. (plus "icon.png" and "info.json" files)
|
|
||||||
if self.url.pathExtension.lowercased() == "apkm" {
|
|
||||||
if let pth = try? zipFile!.unzipFileToTempDir("base.apk") {
|
|
||||||
return (true, ZipFile(pth))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// .apk (and derivatives) have their contents structured directly in zip
|
|
||||||
return (false, zipFile!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract `AndroidManifest.xml` and parse its content
|
|
||||||
func readApkManifest() -> ApkManifest? {
|
|
||||||
assert(type == .APK)
|
|
||||||
let (isApkm, nestedZip) = effectiveApk()
|
|
||||||
guard let data = nestedZip.unzipFile("AndroidManifest.xml") else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let storage = ApkXmlManifestParser()
|
|
||||||
if let xml = try? AndroidXML.init(data: data) {
|
|
||||||
let ALLOWED_TAGS = [ // keep in sync with `ApkXmlManifestParser`
|
|
||||||
"manifest",
|
|
||||||
"application",
|
|
||||||
"uses-feature",
|
|
||||||
"uses-permission",
|
|
||||||
"uses-permission-sdk-23",
|
|
||||||
"uses-sdk",
|
|
||||||
]
|
|
||||||
let parser = xml.parseXml()
|
|
||||||
let ignore = XMLParser()
|
|
||||||
try? parser.iterElements({ startTag, attributes in
|
|
||||||
if ALLOWED_TAGS.contains(startTag) {
|
|
||||||
storage.parser(ignore, didStartElement: startTag, namespaceURI: nil, qualifiedName: nil, attributes: try attributes.asDictStr())
|
|
||||||
}
|
|
||||||
}) { endTag in
|
|
||||||
if ALLOWED_TAGS.contains(endTag) {
|
|
||||||
storage.parser(ignore, didEndElement: endTag, namespaceURI: nil, qualifiedName: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// fallback to xml-string parser
|
|
||||||
let parser = XMLParser(data: data)
|
|
||||||
parser.delegate = storage
|
|
||||||
parser.parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
var rv = storage.result
|
|
||||||
// if apkm can load png, prefer that over xml-parsing
|
|
||||||
if isApkm, let iconData = zipFile!.unzipFile("icon.png") {
|
|
||||||
rv.appIcon = nil
|
|
||||||
rv.appIconData = iconData
|
|
||||||
}
|
|
||||||
os_log(.debug, log: log, "[apk] resolving %{public}@", String(describing: rv))
|
|
||||||
rv.resolve(nestedZip)
|
|
||||||
os_log(.debug, log: log, "[apk] resolved name: \"%{public}@\" icon: %{public}@", rv.appName ?? "", rv.appIcon ?? "-")
|
|
||||||
return rv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrapper to use same code for binary-xml and string-xml parsing
|
|
||||||
private class ApkXmlManifestParser: NSObject, XMLParserDelegate {
|
|
||||||
private var _scope: [String] = []
|
|
||||||
var result = ApkManifest()
|
|
||||||
|
|
||||||
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attrs: [String : String] = [:]) {
|
|
||||||
// keep in sync with ALLOWED_TAGS above
|
|
||||||
switch elementName {
|
|
||||||
case "manifest":
|
|
||||||
if _scope == [] {
|
|
||||||
result.packageId = attrs["package"] // "org.bundle.id"
|
|
||||||
result.versionName = attrs["android:versionName"] // "7.62.3"
|
|
||||||
result.versionCode = attrs["android:versionCode"] // "160700"
|
|
||||||
// attrs["platformBuildVersionCode"] // "35"
|
|
||||||
// attrs["platformBuildVersionName"] // "15"
|
|
||||||
}
|
|
||||||
case "application":
|
|
||||||
if _scope == ["manifest"] {
|
|
||||||
result.appName = attrs["android:label"] // @resource-ref
|
|
||||||
result.appIcon = attrs["android:icon"] // @resource-ref
|
|
||||||
}
|
|
||||||
case "uses-permission", "uses-permission-sdk-23":
|
|
||||||
// no "permission" because that will produce duplicates with "uses-permission"
|
|
||||||
if _scope == ["manifest"], let name = attrs["android:name"] {
|
|
||||||
result.permissions.append(name)
|
|
||||||
}
|
|
||||||
case "uses-feature":
|
|
||||||
if _scope == ["manifest"], let name = attrs["android:name"] {
|
|
||||||
if attrs["android:required"] == "false" {
|
|
||||||
result.featuresOptional.append(name)
|
|
||||||
} else {
|
|
||||||
result.featuresRequired.append(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "uses-sdk":
|
|
||||||
if _scope == ["manifest"] {
|
|
||||||
result.sdkVerMin = Int(attrs["android:minSdkVersion"] ?? "1") // "21"
|
|
||||||
result.sdkVerTarget = Int(attrs["android:targetSdkVersion"] ?? "-1") // "35"
|
|
||||||
}
|
|
||||||
default: break // ignore
|
|
||||||
}
|
|
||||||
_scope.append(elementName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
|
|
||||||
_scope.removeLast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Icon only
|
|
||||||
|
|
||||||
extension MetaInfo {
|
|
||||||
/// Same as `readApkManifest()` but only extract `appIcon`.
|
|
||||||
func readApkIconOnly() -> ApkManifest? {
|
|
||||||
assert(type == .APK)
|
|
||||||
var rv = ApkManifest()
|
|
||||||
let (isApkm, nestedZip) = effectiveApk()
|
|
||||||
if isApkm, let iconData = zipFile!.unzipFile("icon.png") {
|
|
||||||
rv.appIcon = "icon.png"
|
|
||||||
rv.appIconData = iconData
|
|
||||||
return rv
|
|
||||||
}
|
|
||||||
guard let data = nestedZip.unzipFile("AndroidManifest.xml") else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if let xml = try? AndroidXML.init(data: data) {
|
|
||||||
let parser = xml.parseXml()
|
|
||||||
try? parser.iterElements({ startTag, attributes in
|
|
||||||
if startTag == "application" {
|
|
||||||
rv.appIcon = try? attributes.get("android:icon")?.resolve(parser.stringPool)
|
|
||||||
}
|
|
||||||
}) {_ in}
|
|
||||||
} else {
|
|
||||||
// fallback to xml-string parser
|
|
||||||
rv.appIcon = ApkXmlIconParser().run(data)
|
|
||||||
}
|
|
||||||
rv.resolve(nestedZip)
|
|
||||||
return rv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shorter form of `ApkXmlManifestParser` to only exctract the icon reference (used for Thumbnail Provider)
|
|
||||||
private class ApkXmlIconParser: NSObject, XMLParserDelegate {
|
|
||||||
var result: String? = nil
|
|
||||||
|
|
||||||
func run(_ data: Data) -> String? {
|
|
||||||
let parser = XMLParser(data: data)
|
|
||||||
parser.delegate = self
|
|
||||||
parser.parse()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attrs: [String : String] = [:]) {
|
|
||||||
if elementName == "application" {
|
|
||||||
result = attrs["android:icon"]
|
|
||||||
parser.abortParsing()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Resolve resource
|
|
||||||
|
|
||||||
private extension ApkManifest {
|
|
||||||
/// Reuse `ZipFile` from previous call because that may be an already unpacked `base.apk`
|
|
||||||
mutating func resolve(_ zip: ZipFile) {
|
|
||||||
guard let data = zip.unzipFile("resources.arsc"),
|
|
||||||
let xml = try? AndroidXML.init(data: data), xml.type == .Table else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let parser = xml.parseTable()
|
|
||||||
if let val = appName, let ref = try? TblTableRef(val) {
|
|
||||||
appName = parser.getName(ref)
|
|
||||||
}
|
|
||||||
if let val = appIcon, let ref = try? TblTableRef(val) {
|
|
||||||
if let iconPath = parser.getIconDirect(ref) ?? parser.getIconIndirect(ref) {
|
|
||||||
appIcon = iconPath
|
|
||||||
appIconData = zip.unzipFile(iconPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Tbl_Parser {
|
|
||||||
func getName(_ ref: TblTableRef) -> String? {
|
|
||||||
// why the heck are these even allowed?
|
|
||||||
// apparently there can be references onto references
|
|
||||||
var ref = ref
|
|
||||||
while let res = try? self.getResource(ref) {
|
|
||||||
guard let val = res.entries.first?.entry.value else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
switch val.dataType {
|
|
||||||
case .Reference: ref = val.asTableRef // and continue
|
|
||||||
case .String: return val.resolve(self.stringPool)
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lookup resource with matching id. Choose the icon with the highest density.
|
|
||||||
func getIconDirect(_ ref: TblTableRef) -> String? {
|
|
||||||
guard let res = try? self.getResource(ref) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var best: ResValue? = nil
|
|
||||||
var bestScore: UInt16 = 0
|
|
||||||
for e in res.entries {
|
|
||||||
switch e.config.screenType.density {
|
|
||||||
case .Default, .any, .None: continue
|
|
||||||
case let density:
|
|
||||||
if density.rawValue > bestScore, let val = e.entry.value {
|
|
||||||
bestScore = density.rawValue
|
|
||||||
best = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best?.resolve(self.stringPool)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterate over all entries and choose best-rated icon file.
|
|
||||||
/// Rating prefers files which have an attribute name `"app_icon"` or `"ic_launcher"`.
|
|
||||||
func getIconIndirect(_ ref: TblTableRef) -> String? {
|
|
||||||
// sadly we cannot just `getResource()` because that can point to an app banner
|
|
||||||
guard let pkg = try? self.getPackage(ref.package),
|
|
||||||
var pool = pkg.stringPool(for: .Keys),
|
|
||||||
let (_, types) = try? pkg.getType(ref.type) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// density is 120-640
|
|
||||||
let rates: [String: UInt16] = [
|
|
||||||
"app_icon": 1000,
|
|
||||||
"ic_launcher": 800,
|
|
||||||
"ic_launcher_foreground": 200,
|
|
||||||
]
|
|
||||||
var best: ResValue? = nil
|
|
||||||
var bestScore: UInt16 = 0
|
|
||||||
for typ in types {
|
|
||||||
switch typ.config.screenType.density {
|
|
||||||
case .any, .None: continue
|
|
||||||
case let density:
|
|
||||||
try? typ.iterValues {
|
|
||||||
if let val = $1.value {
|
|
||||||
let attrName = pool.getStringCached($1.key)
|
|
||||||
let score = density.rawValue + (rates[attrName] ?? 0)
|
|
||||||
if score > bestScore {
|
|
||||||
bestScore = score
|
|
||||||
best = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best?.resolve(self.stringPool)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,13 +19,13 @@ struct AppIcon {
|
|||||||
case .IPA, .Archive, .Extension:
|
case .IPA, .Archive, .Extension:
|
||||||
extractImage(from: meta.readPlist_Icon()?.filenames)
|
extractImage(from: meta.readPlist_Icon()?.filenames)
|
||||||
case .APK:
|
case .APK:
|
||||||
extractImage(from: meta.readApkIconOnly())
|
extractImage(from: meta.readApk_Icon())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract image from Android app bundle.
|
/// Extract image from Android app bundle.
|
||||||
func extractImage(from manifest: ApkManifest?) -> NSImage {
|
func extractImage(from apkIcon: Apk_Icon?) -> NSImage {
|
||||||
if let data = manifest?.appIconData, let img = NSImage(data: data) {
|
if let data = apkIcon?.data, let img = NSImage(data: data) {
|
||||||
return img
|
return img
|
||||||
}
|
}
|
||||||
return defaultIcon()
|
return defaultIcon()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ extension PreviewGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Process info stored in `AndroidManifest.xml`
|
/// Process info stored in `AndroidManifest.xml`
|
||||||
mutating func procAppInfoAndroid(_ manifest: ApkManifest) {
|
mutating func procAppInfoAndroid(_ manifest: Apk_Manifest) {
|
||||||
let featReq = manifest.featuresRequired
|
let featReq = manifest.featuresRequired
|
||||||
let featOpt = manifest.featuresOptional
|
let featOpt = manifest.featuresOptional
|
||||||
let perms = manifest.permissions
|
let perms = manifest.permissions
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ struct PreviewGenerator {
|
|||||||
data["AppIcon"] = AppIcon(meta).extractImage(from: plistApp.icons).withRoundCorners().asBase64()
|
data["AppIcon"] = AppIcon(meta).extractImage(from: plistApp.icons).withRoundCorners().asBase64()
|
||||||
|
|
||||||
case .APK:
|
case .APK:
|
||||||
guard let manifest = meta.readApkManifest() else {
|
guard let manifest = meta.readApk_Manifest() else {
|
||||||
throw RuntimeError("AndroidManifest.xml not found")
|
throw RuntimeError("AndroidManifest.xml not found")
|
||||||
}
|
}
|
||||||
procAppInfoAndroid(manifest)
|
procAppInfoAndroid(manifest)
|
||||||
// App Icon (last, because the image uses a lot of memory)
|
// App Icon (last, because the image uses a lot of memory)
|
||||||
data["AppIcon"] = AppIcon(meta).extractImage(from: manifest).withRoundCorners().asBase64()
|
data["AppIcon"] = AppIcon(meta).extractImage(from: manifest.icon).withRoundCorners().asBase64()
|
||||||
}
|
}
|
||||||
|
|
||||||
data["QuickLookTitle"] = stringForFileType(meta)
|
data["QuickLookTitle"] = stringForFileType(meta)
|
||||||
|
|||||||
Reference in New Issue
Block a user