Dactyloidae/mobile/ios/Client/Frontend/Browser/OpenInHelper.swift
2026-06-26 21:04:09 -07:00

232 lines
9.1 KiB
Swift

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Foundation
import PassKit
import WebKit
import SnapKit
import Shared
import XCGLogger
private let log = Logger.browserLogger
struct OpenInViewUX {
static let ViewHeight: CGFloat = 40.0
static let TextFont = UIFont.systemFont(ofSize: 16)
static let TextColor = UIColor(red: 74.0/255.0, green: 144.0/255.0, blue: 226.0/255.0, alpha: 1.0)
static let TextOffset = -15
static let OpenInString = NSLocalizedString("Open in…", comment: "String indicating that the file can be opened in another application on the device")
}
enum MimeType: String {
case PDF = "application/pdf"
case PASS = "application/vnd.apple.pkpass"
}
protocol OpenInHelper {
init?(response: URLResponse)
var openInView: UIView? { get set }
func open()
}
struct OpenIn {
static let helpers: [OpenInHelper.Type] = [OpenPdfInHelper.self, OpenPassBookHelper.self, ShareFileHelper.self]
static func helperForResponse(_ response: URLResponse) -> OpenInHelper? {
return helpers.flatMap { $0.init(response: response) }.first
}
}
class ShareFileHelper: NSObject, OpenInHelper {
var openInView: UIView?
fileprivate var url: URL
var pathExtension: String?
required init?(response: URLResponse) {
guard let MIMEType = response.mimeType, !(MIMEType == MimeType.PASS.rawValue || MIMEType == MimeType.PDF.rawValue),
let responseURL = response.url else { return nil }
url = (responseURL as NSURL) as URL
super.init()
}
func open() {
let alertController = UIAlertController(
title: Strings.OpenInDownloadHelperAlertTitle,
message: Strings.OpenInDownloadHelperAlertMessage,
preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction( UIAlertAction(title: Strings.OpenInDownloadHelperAlertCancel, style: .cancel, handler: nil))
alertController.addAction(UIAlertAction(title: Strings.OpenInDownloadHelperAlertConfirm, style: .default) { (action) in
let objectsToShare = [self.url]
let activityVC = UIActivityViewController(activityItems: objectsToShare, applicationActivities: nil)
if let sourceView = self.openInView, let popoverController = activityVC.popoverPresentationController {
popoverController.sourceView = sourceView
popoverController.sourceRect = CGRect(origin: CGPoint(x: sourceView.bounds.midX, y: sourceView.bounds.maxY), size: .zero)
popoverController.permittedArrowDirections = .up
}
UIApplication.shared.keyWindow?.rootViewController?.present(activityVC, animated: true, completion: nil)
})
UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
}
}
class OpenPassBookHelper: NSObject, OpenInHelper {
var openInView: UIView?
fileprivate var url: URL
required init?(response: URLResponse) {
guard let MIMEType = response.mimeType, MIMEType == MimeType.PASS.rawValue && PKAddPassesViewController.canAddPasses(),
let responseURL = response.url else { return nil }
url = responseURL
super.init()
}
func open() {
guard let passData = try? Data(contentsOf: url) else { return }
var error: NSError? = nil
let pass = PKPass(data: passData, error: &error)
if let _ = error {
// display an error
let alertController = UIAlertController(
title: Strings.UnableToAddPassErrorTitle,
message: Strings.UnableToAddPassErrorMessage,
preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(
UIAlertAction(title: Strings.UnableToAddPassErrorDismiss, style: .cancel) { (action) in
// Do nothing.
})
UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
return
}
let passLibrary = PKPassLibrary()
if passLibrary.containsPass(pass) {
UIApplication.shared.openURL(pass.passURL!)
} else {
let addController = PKAddPassesViewController(pass: pass)
UIApplication.shared.keyWindow?.rootViewController?.present(addController, animated: true, completion: nil)
}
}
}
class OpenPdfInHelper: NSObject, OpenInHelper, UIDocumentInteractionControllerDelegate {
fileprivate var url: URL
fileprivate var docController: UIDocumentInteractionController?
fileprivate var openInURL: URL?
lazy var openInView: UIView? = getOpenInView(self)()
lazy var documentDirectory: URL = {
return URL(string: NSTemporaryDirectory())!.appendingPathComponent("pdfs")
}()
fileprivate var filepath: URL?
required init?(response: URLResponse) {
guard let MIMEType = response.mimeType, MIMEType == MimeType.PDF.rawValue && UIApplication.shared.canOpenURL(URL(string: "itms-books:")!),
let responseURL = response.url else { return nil }
url = responseURL
super.init()
setFilePath(response.suggestedFilename ?? url.lastPathComponent )
}
fileprivate func setFilePath(_ suggestedFilename: String) {
var filename = suggestedFilename
let pathExtension = filename.asURL?.pathExtension
if pathExtension == nil {
filename.append(".pdf")
}
filepath = documentDirectory.appendingPathComponent(filename)
}
deinit {
guard let url = openInURL else { return }
let fileManager = FileManager.default
do {
try fileManager.removeItem(at: url)
} catch {
log.error("failed to delete file at \(url): \(error)")
}
}
func getOpenInView() -> OpenInView {
let overlayView = OpenInView()
overlayView.openInButton.addTarget(self, action: #selector(OpenPdfInHelper.open), for: .touchUpInside)
return overlayView
}
func createDocumentControllerForURL(url: URL) {
docController = UIDocumentInteractionController(url: url)
docController?.delegate = self
self.openInURL = url
}
func createLocalCopyOfPDF() {
guard let filePath = filepath else {
log.error("failed to create proper URL")
return
}
if docController == nil {
// if we already have a URL but no document controller, just create the document controller
if let url = openInURL {
createDocumentControllerForURL(url: url)
return
}
let contentsOfFile = try? Data(contentsOf: url)
let fileManager = FileManager.default
do {
try fileManager.createDirectory(atPath: documentDirectory.absoluteString, withIntermediateDirectories: true, attributes: nil)
if fileManager.createFile(atPath: filePath.absoluteString, contents: contentsOfFile, attributes: nil) {
let openInURL = URL(fileURLWithPath: filePath.absoluteString)
createDocumentControllerForURL(url: openInURL)
} else {
log.error("Unable to create local version of PDF file at \(filePath)")
}
} catch {
log.error("Error on creating directory at \(self.documentDirectory)")
}
}
}
func open() {
createLocalCopyOfPDF()
guard let _parentView = self.openInView!.superview, let docController = self.docController else { log.error("view doesn't have a superview so can't open anything"); return }
// iBooks should be installed by default on all devices we care about, so regardless of whether or not there are other pdf-capable
// apps on this device, if we can open in iBooks we can open this PDF
// simulators do not have iBooks so the open in view will not work on the simulator
if UIApplication.shared.canOpenURL(URL(string: "itms-books:")!) {
log.info("iBooks installed: attempting to open pdf")
docController.presentOpenInMenu(from: .zero, in: _parentView, animated: true)
} else {
log.info("iBooks is not installed")
}
}
}
class OpenInView: UIView {
let openInButton = UIButton()
init() {
super.init(frame: .zero)
openInButton.setTitleColor(OpenInViewUX.TextColor, for: UIControlState.normal)
openInButton.setTitle(OpenInViewUX.OpenInString, for: UIControlState.normal)
openInButton.titleLabel?.font = OpenInViewUX.TextFont
openInButton.sizeToFit()
self.addSubview(openInButton)
openInButton.snp.makeConstraints { make in
make.centerY.equalTo(self)
make.height.equalTo(self)
make.trailing.equalTo(self).offset(OpenInViewUX.TextOffset)
}
self.backgroundColor = UIColor.white
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}