ref: ApkManifest
This commit is contained in:
@@ -1038,7 +1038,7 @@
|
||||
repositoryURL = "https://github.com/relikd/AndroidXML";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 1.3.0;
|
||||
version = 1.4.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/relikd/AndroidXML",
|
||||
"state" : {
|
||||
"revision" : "fd522d612f24ee813c80b1a1b0f6bd311b2735c3",
|
||||
"version" : "1.3.0"
|
||||
"revision" : "d9fe646bcc3b05548aebbd20b4eee0af675c129f",
|
||||
"version" : "1.4.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -16,16 +16,9 @@ extension QLThumbnailReply {
|
||||
}
|
||||
|
||||
class ThumbnailProvider: QLThumbnailProvider {
|
||||
|
||||
// TODO: sadly, this does not seem to work for .xcarchive and .appex
|
||||
// Probably overwritten by Apple somehow
|
||||
|
||||
override func provideThumbnail(for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) {
|
||||
let meta = MetaInfo(request.fileURL)
|
||||
guard let appPlist = meta.readPlistApp(iconOnly: true) else {
|
||||
return
|
||||
}
|
||||
let img = AppIcon(meta).extractImage(from: appPlist).withRoundCorners()
|
||||
let img = AppIcon(meta).extractImageForThumbnail().withRoundCorners()
|
||||
|
||||
// First way: Draw the thumbnail into the current context, set up with UIKit's coordinate system.
|
||||
let reply = QLThumbnailReply(contextSize: request.maximumSize, currentContextDrawing: { () -> Bool in
|
||||
|
||||
@@ -13,17 +13,27 @@ struct AppIcon {
|
||||
self.meta = meta
|
||||
}
|
||||
|
||||
/// Convenience getter to extract app icon regardless of bundle-type.
|
||||
func extractImageForThumbnail() -> NSImage {
|
||||
switch meta.type {
|
||||
case .IPA, .Archive, .Extension:
|
||||
extractImage(from: meta.readPlistApp())
|
||||
case .APK:
|
||||
extractImage(from: meta.readApkIconOnly())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract image from Android app bundle.
|
||||
func extractImage(from manifest: ApkManifest?) -> NSImage {
|
||||
if let data = manifest?.appIconData, let img = NSImage(data: data) {
|
||||
return img
|
||||
}
|
||||
return defaultIcon()
|
||||
}
|
||||
|
||||
/// Try multiple methods to extract image.
|
||||
/// This method will always return an image even if none is found, in which case it returns the default image.
|
||||
func extractImage(from appPlist: PlistDict?) -> NSImage {
|
||||
if meta.type == .APK {
|
||||
if let iconPath = appPlist?["appIcon"] as? String,
|
||||
let data = meta.zipFile!.unzipFile(iconPath),
|
||||
let img = NSImage(data: data) {
|
||||
return img
|
||||
}
|
||||
return defaultIcon()
|
||||
}
|
||||
// no need to unwrap the plist, and most .ipa should include the Artwork anyway
|
||||
if meta.type == .IPA {
|
||||
if let data = meta.zipFile!.unzipFile("iTunesArtwork") {
|
||||
|
||||
@@ -4,12 +4,30 @@ import os // OSLog
|
||||
|
||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "MetaInfo+Apk")
|
||||
|
||||
/// Representation of `AndroidManifest.xml`
|
||||
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: String? = nil
|
||||
var sdkVerTarget: String? = nil
|
||||
|
||||
var featuresRequired: [String] = []
|
||||
var featuresOptional: [String] = []
|
||||
var permissions: [String] = []
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Full Manifest
|
||||
|
||||
extension MetaInfo {
|
||||
/// Extract `AndroidManifest.xml` and parse its content
|
||||
func readApkManifest() -> PlistDict? {
|
||||
func readApkManifest() -> ApkManifest? {
|
||||
assert(type == .APK)
|
||||
guard let data = self.readPayloadFile("AndroidManifest.xml", osxSubdir: nil) else {
|
||||
return nil
|
||||
}
|
||||
@@ -42,14 +60,9 @@ extension MetaInfo {
|
||||
}
|
||||
|
||||
var rv = storage.result
|
||||
guard let _ = rv["packageId"] else {
|
||||
return nil
|
||||
}
|
||||
os_log(.debug, log: log, "[apk] resolving resources name: %{public}@ icon: %{public}@", String(describing: rv["appName"]), String(describing: rv["appIcon"]))
|
||||
let resolved = self.resolveResources([(ResourceType.Name, rv["appName"] as? String), (ResourceType.Icon, rv["appIcon"] as? String)])
|
||||
os_log(.debug, log: log, "[apk] resolved %{public}@", String(describing: resolved))
|
||||
rv["appName"] = resolved[0]
|
||||
rv["appIcon"] = resolved[1]
|
||||
os_log(.debug, log: log, "[apk] resolving %{public}@", String(describing: rv))
|
||||
rv.resolve(zipFile!)
|
||||
os_log(.debug, log: log, "[apk] resolved name: \"%{public}@\" icon: %{public}@", rv.appName ?? "", rv.appIcon ?? "-")
|
||||
return rv
|
||||
}
|
||||
}
|
||||
@@ -57,48 +70,41 @@ extension MetaInfo {
|
||||
/// Wrapper to use same code for binary-xml and string-xml parsing
|
||||
private class ApkXmlManifestParser: NSObject, XMLParserDelegate {
|
||||
private var _scope: [String] = []
|
||||
private var _rv: [String: String] = [:]
|
||||
private var _perm: [String] = []
|
||||
private var _featOpt: [String] = []
|
||||
private var _featReq: [String] = []
|
||||
var result: PlistDict {
|
||||
(_rv as PlistDict).merging([
|
||||
"permissions": _perm,
|
||||
"featuresOptional": _featOpt,
|
||||
"featuresRequired": _featReq,
|
||||
]) { $1 }
|
||||
}
|
||||
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 == [] {
|
||||
_rv["packageId"] = attrs["package"] // "org.bundle.id"
|
||||
_rv["versionName"] = attrs["android:versionName"] // "7.62.3"
|
||||
_rv["versionCode"] = attrs["android:versionCode"] // "160700"
|
||||
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"] {
|
||||
_rv["appName"] = attrs["android:label"] // @resource-ref
|
||||
_rv["appIcon"] = attrs["android:icon"] // @resource-ref
|
||||
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"] {
|
||||
_perm.append(name)
|
||||
result.permissions.append(name)
|
||||
}
|
||||
case "uses-feature":
|
||||
if _scope == ["manifest"], let name = attrs["android:name"] {
|
||||
let optional = attrs["android:required"] == "false"
|
||||
optional ? _featOpt.append(name) : _featReq.append(name)
|
||||
if attrs["android:required"] == "false" {
|
||||
result.featuresOptional.append(name)
|
||||
} else {
|
||||
result.featuresRequired.append(name)
|
||||
}
|
||||
}
|
||||
case "uses-sdk":
|
||||
if _scope == ["manifest"] {
|
||||
_rv["sdkVerMin"] = attrs["android:minSdkVersion"] ?? "1" // "21"
|
||||
_rv["sdkVerTarget"] = attrs["android:targetSdkVersion"] // "35"
|
||||
result.sdkVerMin = attrs["android:minSdkVersion"] ?? "1" // "21"
|
||||
result.sdkVerTarget = attrs["android:targetSdkVersion"] // "35"
|
||||
}
|
||||
default: break // ignore
|
||||
}
|
||||
@@ -115,26 +121,25 @@ private class ApkXmlManifestParser: NSObject, XMLParserDelegate {
|
||||
|
||||
extension MetaInfo {
|
||||
/// Same as `readApkManifest()` but only extract `appIcon`.
|
||||
func readApkIconOnly() -> PlistDict? {
|
||||
func readApkIconOnly() -> ApkManifest? {
|
||||
assert(type == .APK)
|
||||
var rv = ApkManifest()
|
||||
guard let data = self.readPayloadFile("AndroidManifest.xml", osxSubdir: nil) else {
|
||||
return nil
|
||||
}
|
||||
var icon: String? = nil
|
||||
if let xml = try? AndroidXML.init(data: data) {
|
||||
let parser = xml.parseXml()
|
||||
try? parser.iterElements({ startTag, attributes in
|
||||
if startTag == "application" {
|
||||
icon = try? attributes.asDictStr()["android:icon"]
|
||||
rv.appIcon = try? attributes.get("android:icon")?.resolve(parser.stringPool)
|
||||
}
|
||||
}) {_ in}
|
||||
} else {
|
||||
// fallback to xml-string parser
|
||||
icon = ApkXmlIconParser().run(data)
|
||||
rv.appIcon = ApkXmlIconParser().run(data)
|
||||
}
|
||||
if let icon = self.resolveResources([(.Icon, icon)])[0] {
|
||||
return ["appIcon": icon]
|
||||
}
|
||||
return nil
|
||||
rv.resolve(zipFile!)
|
||||
return rv
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,25 +165,21 @@ private class ApkXmlIconParser: NSObject, XMLParserDelegate {
|
||||
|
||||
// MARK: - Resolve resource
|
||||
|
||||
private extension MetaInfo {
|
||||
// currently there are only two types of resources, "android:icon" and "android:label"
|
||||
enum ResourceType {
|
||||
case Name
|
||||
case Icon
|
||||
}
|
||||
func resolveResources(_ ids: [(ResourceType, String?)]) -> [String?] {
|
||||
guard let data = self.readPayloadFile("resources.arsc", osxSubdir: nil),
|
||||
private extension ApkManifest {
|
||||
mutating func resolve(_ zip: ZipFile) {
|
||||
guard let data = zip.unzipFile("resources.arsc"),
|
||||
let xml = try? AndroidXML.init(data: data), xml.type == .Table else {
|
||||
return ids.map { _ in nil }
|
||||
return
|
||||
}
|
||||
|
||||
let parser = xml.parseTable()
|
||||
return ids.map { typ, val in
|
||||
guard let val, let ref = try? TblTableRef(val) else {
|
||||
return nil
|
||||
}
|
||||
switch typ {
|
||||
case .Name: return parser.getName(ref)
|
||||
case .Icon: return parser.getIconDirect(ref) ?? parser.getIconIndirect(ref)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,12 +88,12 @@ struct MetaInfo {
|
||||
}
|
||||
|
||||
/// Read app default `Info.plist`. (used for both, Preview and Thumbnail)
|
||||
func readPlistApp(iconOnly: Bool = false) -> PlistDict? {
|
||||
func readPlistApp() -> PlistDict? {
|
||||
switch self.type {
|
||||
case .IPA, .Archive, .Extension:
|
||||
return self.readPayloadFile("Info.plist", osxSubdir: nil)?.asPlistOrNil()
|
||||
case .APK:
|
||||
return iconOnly ? self.readApkIconOnly() : self.readApkManifest()
|
||||
return nil // not applicable for Android
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@ extension PreviewGenerator {
|
||||
}
|
||||
|
||||
/// Process info stored in `Info.plist`
|
||||
mutating func procAppInfoAndroid(_ appPlist: PlistDict) {
|
||||
let featuresRequired = appPlist["featuresRequired"] as! [String]
|
||||
let featuresOptional = appPlist["featuresOptional"] as! [String]
|
||||
let permissions = appPlist["permissions"] as! [String]
|
||||
mutating func procAppInfoAndroid(_ manifest: ApkManifest) {
|
||||
let featReq = manifest.featuresRequired
|
||||
let featOpt = manifest.featuresOptional
|
||||
let perms = manifest.permissions
|
||||
|
||||
func asList(_ list: [String]) -> String {
|
||||
"<pre>\(list.joined(separator: "\n"))</pre>"
|
||||
@@ -58,21 +58,21 @@ extension PreviewGenerator {
|
||||
}
|
||||
self.apply([
|
||||
"AppInfoHidden": CLASS_VISIBLE,
|
||||
"AppName": appPlist["appName"] as? String ?? "",
|
||||
"AppVersion": appPlist["versionName"] as? String ?? "",
|
||||
"AppBuildVer": appPlist["versionCode"] as? String ?? "",
|
||||
"AppId": appPlist["packageId"] as? String ?? "",
|
||||
"AppName": manifest.appName ?? "",
|
||||
"AppVersion": manifest.versionName ?? "",
|
||||
"AppBuildVer": manifest.versionCode ?? "",
|
||||
"AppId": manifest.packageId ?? "",
|
||||
|
||||
"ApkFeaturesRequiredHidden": featuresRequired.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||
"ApkFeaturesRequiredList": asList(featuresRequired),
|
||||
"ApkFeaturesOptionalHidden": featuresOptional.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||
"ApkFeaturesOptionalList": asList(featuresOptional),
|
||||
"ApkPermissionsHidden": permissions.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||
"ApkPermissionsList": asList(permissions),
|
||||
"ApkFeaturesRequiredHidden": featReq.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||
"ApkFeaturesRequiredList": asList(featReq),
|
||||
"ApkFeaturesOptionalHidden": featOpt.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||
"ApkFeaturesOptionalList": asList(featOpt),
|
||||
"ApkPermissionsHidden": perms.isEmpty ? CLASS_HIDDEN : CLASS_VISIBLE,
|
||||
"ApkPermissionsList": asList(perms),
|
||||
|
||||
"AppDeviceFamily": "Android",
|
||||
"AppSDK": resolveSDK(appPlist["sdkVerTarget"] as? String),
|
||||
"AppMinOS": resolveSDK(appPlist["sdkVerMin"] as? String),
|
||||
"AppSDK": resolveSDK(manifest.sdkVerTarget),
|
||||
"AppMinOS": resolveSDK(manifest.sdkVerMin),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,15 +23,12 @@ struct PreviewGenerator {
|
||||
|
||||
init(_ meta: MetaInfo) throws {
|
||||
self.meta = meta
|
||||
guard let plistApp = meta.readPlistApp() else {
|
||||
let isAndroid = meta.type == .APK
|
||||
throw RuntimeError(isAndroid ? "AndroidManifest.xml not found" : "Info.plist not found")
|
||||
}
|
||||
|
||||
data["QuickLookTitle"] = stringForFileType(meta)
|
||||
|
||||
switch meta.type {
|
||||
case .IPA, .Archive, .Extension:
|
||||
guard let plistApp = meta.readPlistApp() else {
|
||||
throw RuntimeError("Info.plist not found")
|
||||
}
|
||||
procAppInfoApple(plistApp, isOSX: meta.isOSX)
|
||||
if meta.type == .IPA {
|
||||
procItunesMeta(meta.readPlistItunes())
|
||||
@@ -43,14 +40,21 @@ struct PreviewGenerator {
|
||||
let plistProvision = meta.readPlistProvision()
|
||||
procEntitlements(meta, plistApp, plistProvision)
|
||||
procProvision(plistProvision, isOSX: meta.isOSX)
|
||||
// App Icon (last, because the image uses a lot of memory)
|
||||
data["AppIcon"] = AppIcon(meta).extractImage(from: plistApp).withRoundCorners().asBase64()
|
||||
|
||||
case .APK:
|
||||
procAppInfoAndroid(plistApp)
|
||||
guard let manifest = meta.readApkManifest() else {
|
||||
throw RuntimeError("AndroidManifest.xml not found")
|
||||
}
|
||||
procAppInfoAndroid(manifest)
|
||||
// App Icon (last, because the image uses a lot of memory)
|
||||
data["AppIcon"] = AppIcon(meta).extractImage(from: manifest).withRoundCorners().asBase64()
|
||||
}
|
||||
|
||||
data["QuickLookTitle"] = stringForFileType(meta)
|
||||
procFileInfo(meta.url)
|
||||
procFooterInfo()
|
||||
// App Icon (last, because the image uses a lot of memory)
|
||||
data["AppIcon"] = AppIcon(meta).extractImage(from: plistApp).withRoundCorners().asBase64()
|
||||
}
|
||||
|
||||
mutating func apply(_ values: [String: String]) {
|
||||
|
||||
Reference in New Issue
Block a user