diff --git a/App/Info.plist b/App/Info.plist index dbd05c7..697feb7 100644 --- a/App/Info.plist +++ b/App/Info.plist @@ -43,6 +43,7 @@ public.filename-extension apk + apkm diff --git a/QLPreview/Info.plist b/QLPreview/Info.plist index 9f056ce..2fe4c84 100644 --- a/QLPreview/Info.plist +++ b/QLPreview/Info.plist @@ -18,6 +18,7 @@ com.google.android.apk dyn.ah62d4rv4ge80c6dp public.archive.apk + dyn.ah62d4rv4ge80c6dpry QLSupportsSearchableItems diff --git a/QLThumbnail/Info.plist b/QLThumbnail/Info.plist index f97ccc2..75412b0 100644 --- a/QLThumbnail/Info.plist +++ b/QLThumbnail/Info.plist @@ -16,6 +16,7 @@ com.google.android.apk dyn.ah62d4rv4ge80c6dp public.archive.apk + dyn.ah62d4rv4ge80c6dpry QLThumbnailMinimumDimension 16 diff --git a/src/MetaInfo+Apk.swift b/src/MetaInfo+Apk.swift index 7629dd3..9d2cf5e 100644 --- a/src/MetaInfo+Apk.swift +++ b/src/MetaInfo+Apk.swift @@ -25,10 +25,24 @@ struct ApkManifest { // MARK: - Full Manifest extension MetaInfo { + /// `(true, )` -- 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) - guard let data = self.readPayloadFile("AndroidManifest.xml", osxSubdir: nil) else { + let (isApkm, nestedZip) = effectiveApk() + guard let data = nestedZip.unzipFile("AndroidManifest.xml") else { return nil } let storage = ApkXmlManifestParser() @@ -60,8 +74,13 @@ extension MetaInfo { } 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(zipFile!) + rv.resolve(nestedZip) os_log(.debug, log: log, "[apk] resolved name: \"%{public}@\" icon: %{public}@", rv.appName ?? "", rv.appIcon ?? "-") return rv } @@ -124,7 +143,13 @@ extension MetaInfo { func readApkIconOnly() -> ApkManifest? { assert(type == .APK) var rv = ApkManifest() - guard let data = self.readPayloadFile("AndroidManifest.xml", osxSubdir: nil) else { + 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) { @@ -138,7 +163,7 @@ extension MetaInfo { // fallback to xml-string parser rv.appIcon = ApkXmlIconParser().run(data) } - rv.resolve(zipFile!) + rv.resolve(nestedZip) return rv } } @@ -166,6 +191,7 @@ private class ApkXmlIconParser: NSObject, XMLParserDelegate { // 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 { diff --git a/src/MetaInfo.swift b/src/MetaInfo.swift index 918b696..8d78553 100644 --- a/src/MetaInfo.swift +++ b/src/MetaInfo.swift @@ -33,7 +33,7 @@ struct MetaInfo { var zipFile: ZipFile? = nil switch self.UTI { - case "com.apple.itunes.ipa", "com.opa334.trollstore.tipa", "dyn.ah62d4rv4ge81k4puqe": + case "com.apple.itunes.ipa", "com.opa334.trollstore.tipa", "dyn.ah62d4rv4ge81k4puqe" /* tipa */: self.type = FileType.IPA zipFile = ZipFile(self.url.path) case "com.apple.xcode.archive": @@ -47,10 +47,9 @@ struct MetaInfo { } case "com.apple.application-and-system-extension": self.type = FileType.Extension - case "com.google.android.apk", "dyn.ah62d4rv4ge80c6dp", "public.archive.apk": + case "com.google.android.apk", "dyn.ah62d4rv4ge80c6dp" /* apk */, "public.archive.apk", "dyn.ah62d4rv4ge80c6dpry" /* apkm */: self.type = FileType.APK zipFile = ZipFile(self.url.path) -// case "com.google.android.apkm", "dyn.ah62d4rv4ge80c6dpry": default: os_log(.error, log: log, "Unsupported file type: %{public}@", self.UTI) fatalError() @@ -81,7 +80,7 @@ struct MetaInfo { case .IPA: return zipFile!.unzipFile("Payload/*.app/".appending(filename)) case .APK: - return zipFile!.unzipFile(filename) + return nil // not applicable for .apk case .Archive, .Extension: return try? Data(contentsOf: self.effectiveUrl(osxSubdir, filename)) }