forked from shadowfacts/Tusker
317 lines
14 KiB
Swift
317 lines
14 KiB
Swift
//
|
|
// AppDelegate.swift
|
|
// Tusker
|
|
//
|
|
// Created by Shadowfacts on 8/15/18.
|
|
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import CoreData
|
|
import OSLog
|
|
#if canImport(Sentry)
|
|
import Sentry
|
|
#endif
|
|
import UserAccounts
|
|
import ComposeUI
|
|
import TuskerPreferences
|
|
import PushNotifications
|
|
|
|
typealias Preferences = TuskerPreferences.Preferences
|
|
|
|
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
|
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppDelegate")
|
|
|
|
@main
|
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
|
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
#if canImport(Sentry)
|
|
configureSentry()
|
|
#endif
|
|
#if !os(visionOS)
|
|
swizzleStatusBar()
|
|
swizzlePresentationController()
|
|
#endif
|
|
|
|
AppShortcutItem.createItems(for: application)
|
|
|
|
if let oldSavedData = SavedDataManager.load() {
|
|
do {
|
|
for account in oldSavedData.accountIDs {
|
|
guard let account = UserAccountsManager.shared.getAccount(id: account) else {
|
|
continue
|
|
}
|
|
let controller = MastodonController.getForAccount(account)
|
|
try oldSavedData.migrateToCoreData(accountID: account.id, context: controller.persistentContainer.viewContext)
|
|
if controller.persistentContainer.viewContext.hasChanges {
|
|
try controller.persistentContainer.viewContext.save()
|
|
}
|
|
}
|
|
try SavedDataManager.destroy()
|
|
} catch {
|
|
// no-op
|
|
}
|
|
}
|
|
|
|
// make sure the persistent container is initialized on the main thread
|
|
// otherwise initializing it on the background thread can deadlock with accessing it on the main thread elsewhere
|
|
#if canImport(Sentry)
|
|
DraftsPersistentContainer.captureError = { SentrySDK.capture(error: $0) }
|
|
#endif
|
|
_ = DraftsPersistentContainer.shared
|
|
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
let oldDraftsFile = documentsDirectory.appendingPathComponent("drafts").appendingPathExtension("plist")
|
|
let appGroupDraftsFile = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!.appendingPathComponent("drafts").appendingPathExtension("plist")
|
|
for url in [oldDraftsFile, appGroupDraftsFile] where FileManager.default.fileExists(atPath: url.path) {
|
|
DraftsPersistentContainer.shared.migrate(from: url) {
|
|
if case .failure(let error) = $0 {
|
|
#if canImport(Sentry)
|
|
SentrySDK.capture(error: error)
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
BackgroundManager.shared.registerHandlers()
|
|
|
|
initializePushNotifications()
|
|
|
|
return true
|
|
}
|
|
|
|
#if canImport(Sentry)
|
|
private func configureSentry() {
|
|
guard let info = Bundle.main.object(forInfoDictionaryKey: "TuskerInfo") as? [String: Any],
|
|
let dsn = info["SentryDSN"] as? String,
|
|
!dsn.isEmpty else {
|
|
return
|
|
}
|
|
SentrySDK.start { options in
|
|
#if DEBUG
|
|
options.debug = true
|
|
options.environment = "dev"
|
|
#endif
|
|
|
|
// the '//' in the full url can't be escaped, so we have to add the scheme back
|
|
options.dsn = "https://\(dsn)"
|
|
|
|
options.enableSwizzling = false
|
|
// required to support releases/release health
|
|
options.enableAutoSessionTracking = true
|
|
options.enableWatchdogTerminationTracking = false
|
|
options.enableAutoPerformanceTracing = false
|
|
options.enableNetworkTracking = false
|
|
options.enableAppHangTracking = false
|
|
options.enableCoreDataTracing = false
|
|
// we don't care about events like battery, keyboard show/hide
|
|
options.enableAutoBreadcrumbTracking = false
|
|
options.enableUserInteractionTracing = false
|
|
options.profilesSampleRate = nil
|
|
options.tracesSampleRate = nil
|
|
|
|
options.beforeSend = { event in
|
|
// just no, why would anyone need this information
|
|
event.context?.removeValue(forKey: "culture")
|
|
return Preferences.shared.reportErrorsAutomatically ? event : nil
|
|
}
|
|
|
|
if let clazz = NSClassFromString("SentryInstallation"),
|
|
let objClazz = clazz as AnyObject as? NSObject,
|
|
let id = objClazz.perform(Selector(("idWithCacheDirectoryPath:")), with: options.cacheDirectoryPath).takeUnretainedValue() as? String {
|
|
logger.info("Initialized Sentry with installation/user ID: \(id, privacy: .public)")
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
override func buildMenu(with builder: UIMenuBuilder) {
|
|
if builder.system == .main {
|
|
MenuController.buildMainMenu(builder: builder)
|
|
}
|
|
}
|
|
|
|
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
|
let name = getSceneNameForActivity(options.userActivities.first)
|
|
return UISceneConfiguration(name: name, sessionRole: connectingSceneSession.role)
|
|
}
|
|
|
|
private func getSceneNameForActivity(_ activity: NSUserActivity?) -> String {
|
|
guard let activity = activity,
|
|
let type = UserActivityType(rawValue: activity.activityType) else {
|
|
return "main-scene"
|
|
}
|
|
|
|
switch type {
|
|
case .mainScene:
|
|
return "main-scene"
|
|
|
|
case .showConversation,
|
|
.showTimeline,
|
|
.checkNotifications,
|
|
.search,
|
|
.bookmarks,
|
|
.myProfile,
|
|
.showProfile,
|
|
.showNotification:
|
|
if activity.displaysAuxiliaryScene {
|
|
stateRestorationLogger.info("Using auxiliary scene for \(type.rawValue, privacy: .public)")
|
|
return "auxiliary"
|
|
} else {
|
|
return "main-scene"
|
|
}
|
|
|
|
case .newPost:
|
|
return "compose"
|
|
}
|
|
}
|
|
|
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
PushManager.shared.didRegisterForRemoteNotifications(deviceToken: deviceToken)
|
|
}
|
|
|
|
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
|
|
PushManager.shared.didFailToRegisterForRemoteNotifications(error: error)
|
|
}
|
|
|
|
private func initializePushNotifications() {
|
|
UNUserNotificationCenter.current().delegate = self
|
|
Task {
|
|
#if canImport(Sentry)
|
|
PushManager.captureError = { SentrySDK.capture(error: $0) }
|
|
#endif
|
|
await PushManager.shared.updateIfNecessary(updateSubscription: {
|
|
guard let account = UserAccountsManager.shared.getAccount(id: $0.accountID) else {
|
|
return false
|
|
}
|
|
let mastodonController = MastodonController.getForAccount(account)
|
|
do {
|
|
let result = try await mastodonController.updatePushSubscription(subscription: $0)
|
|
PushManager.logger.info("Updated push subscription \(result.id, privacy: .public) on \(mastodonController.instanceURL) with endpoint \($0.endpoint, privacy: .public)")
|
|
PushManager.logger.debug("New push subscription: \(String(describing: result))")
|
|
return true
|
|
} catch {
|
|
PushManager.logger.error("Error updating push subscription: \(String(describing: error))")
|
|
return false
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
#if !os(visionOS)
|
|
private func swizzleStatusBar() {
|
|
let selector = Selector(("handleTapAction:"))
|
|
var originalIMP: IMP?
|
|
let imp = imp_implementationWithBlock({ (self: UIStatusBarManager, sender: AnyObject) in
|
|
let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIStatusBarManager, Selector, AnyObject) -> Void).self)
|
|
let exception = catchNSException {
|
|
guard let windowScene = self.perform(Selector(("windowScene"))).takeUnretainedValue() as? UIWindowScene,
|
|
let xPosition = sender.value(forKey: "xPosition") as? CGFloat,
|
|
let delegate = windowScene.delegate as? TuskerSceneDelegate else {
|
|
original(self, selector, sender)
|
|
return
|
|
}
|
|
switch delegate.handleStatusBarTapped(xPosition: xPosition) {
|
|
case .stop:
|
|
return
|
|
case .continue:
|
|
original(self, selector, sender)
|
|
}
|
|
}
|
|
if let exception {
|
|
SentrySDK.capture(exception: exception)
|
|
}
|
|
} as @convention(block) (UIStatusBarManager, AnyObject) -> Void)
|
|
originalIMP = class_replaceMethod(UIStatusBarManager.self, selector, imp, "v@:@")
|
|
if originalIMP == nil {
|
|
Logging.general.error("Unable to swizzle status bar manager")
|
|
}
|
|
}
|
|
|
|
@available(iOS, obsoleted: 17.0)
|
|
private func swizzlePresentationController() {
|
|
guard #unavailable(iOS 17.0) else {
|
|
return
|
|
}
|
|
var originalIMP: IMP?
|
|
let imp = imp_implementationWithBlock({ (self: UIPresentationController) in
|
|
let new = UITraitCollection(pureBlackDarkMode: self.presentingViewController.traitCollection.pureBlackDarkMode)
|
|
if let existing = self.overrideTraitCollection {
|
|
self.overrideTraitCollection = UITraitCollection(traitsFrom: [existing, new])
|
|
} else {
|
|
self.overrideTraitCollection = new
|
|
}
|
|
let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIPresentationController) -> Void).self)
|
|
original(self)
|
|
} as (@convention(block) (UIPresentationController) -> Void))
|
|
let sel = Selector(["Necessary", "If", "Traits", "update", "_"].reversed().joined())
|
|
originalIMP = class_replaceMethod(UIPresentationController.self, sel, imp, "v@:")
|
|
if originalIMP == nil {
|
|
Logging.general.error("Unable to swizzle presentation controller")
|
|
}
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
extension AppDelegate: UNUserNotificationCenterDelegate {
|
|
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
|
let mainSceneDelegate: MainSceneDelegate
|
|
if let delegate = UIApplication.shared.activeScene?.delegate as? MainSceneDelegate {
|
|
mainSceneDelegate = delegate
|
|
} else if let scene = UIApplication.shared.connectedScenes.first(where: { $0.delegate is MainSceneDelegate }) {
|
|
mainSceneDelegate = scene.delegate as! MainSceneDelegate
|
|
} else if let accountID = UserAccountsManager.shared.mostRecentAccountID {
|
|
let activity = UserActivityManager.mainSceneActivity(accountID: accountID)
|
|
activity.addUserInfoEntries(from: ["showNotificationsPreferences": true])
|
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil)
|
|
return
|
|
} else {
|
|
// without an account, we can't do anything
|
|
return
|
|
}
|
|
mainSceneDelegate.showNotificationsPreferences()
|
|
}
|
|
|
|
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
|
completionHandler(.banner)
|
|
}
|
|
|
|
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
|
let userInfo = response.notification.request.content.userInfo
|
|
guard let notificationID = userInfo["notificationID"] as? String,
|
|
let accountID = userInfo["accountID"] as? String,
|
|
let account = UserAccountsManager.shared.getAccount(id: accountID) else {
|
|
return
|
|
}
|
|
if let scene = response.targetScene,
|
|
let delegate = scene.delegate as? MainSceneDelegate,
|
|
let rootViewController = delegate.rootViewController {
|
|
let mastodonController = MastodonController.getForAccount(account)
|
|
|
|
// if the scene is already active, then we animate things
|
|
let animated = scene.activationState == .foregroundActive
|
|
|
|
delegate.activateAccount(account, animated: animated)
|
|
|
|
rootViewController.runNavigation(animated: animated) { navigation in
|
|
navigation.select(route: .notifications)
|
|
let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)
|
|
navigation.push(viewController: vc)
|
|
}
|
|
} else {
|
|
let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID)
|
|
if #available(iOS 17.0, *) {
|
|
let request = UISceneSessionActivationRequest(userActivity: activity)
|
|
UIApplication.shared.activateSceneSession(for: request)
|
|
} else {
|
|
UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil)
|
|
}
|
|
}
|
|
completionHandler()
|
|
}
|
|
}
|