chore: move files around
This commit is contained in:
45
src/Data - Android/AndroidSdkMap.swift
Normal file
45
src/Data - Android/AndroidSdkMap.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
// see https://developer.android.com/guide/topics/manifest/uses-sdk-element#api-level-table
|
||||
|
||||
let AndroidSdkMap: [Int: String] = [
|
||||
1: "1.0",
|
||||
2: "1.1",
|
||||
3: "1.5",
|
||||
4: "1.6",
|
||||
5: "2.0",
|
||||
6: "2.0.1",
|
||||
7: "2.1.x",
|
||||
8: "2.2.x",
|
||||
9: "2.3, 2.3.1, 2.3.2",
|
||||
10: "2.3.3, 2.3.4",
|
||||
11: "3.0.x",
|
||||
12: "3.1.x",
|
||||
13: "3.2",
|
||||
14: "4.0, 4.0.1, 4.0.2",
|
||||
15: "4.0.3, 4.0.4",
|
||||
16: "4.1, 4.1.1",
|
||||
17: "4.2, 4.2.2",
|
||||
18: "4.3",
|
||||
19: "4.4",
|
||||
20: "4.4W",
|
||||
21: "5.0",
|
||||
22: "5.1",
|
||||
23: "6.0",
|
||||
24: "7.0",
|
||||
25: "7.1, 7.1.1",
|
||||
26: "8.0",
|
||||
27: "8.1",
|
||||
28: "9",
|
||||
29: "10",
|
||||
30: "11",
|
||||
31: "12",
|
||||
32: "12",
|
||||
33: "13",
|
||||
34: "14",
|
||||
35: "15",
|
||||
36: "16",
|
||||
// can we assume new versions will stick to this scheme?
|
||||
37: "17",
|
||||
38: "18",
|
||||
39: "19",
|
||||
40: "20",
|
||||
]
|
||||
74
src/Data - Android/Apk+Icon.swift
Normal file
74
src/Data - Android/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/Data - Android/Apk+Manifest.swift
Normal file
123
src/Data - Android/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/Data - Android/Apk.swift
Normal file
130
src/Data - Android/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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user