mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
* Change error from Swift.Error to NSError
* Adds ResultOperation.localizedFailure
* Finish Riley's monster commit
3b38d725d7
May the Gods have mercy on my soul.
* Fix format strings I broke
* Include "Enable JIT" errors in Error Log
* Fix minimuxer status checking
* [skip ci] Update the no wifi message to include VPN
* Opens Error Log when tapping ToastView
* Fixes Error Log context menu covering cell content
* Fixes Error Log context menu appearing while scrolling
* Fixes incorrect Search FAQ URL
* Fix Error Log showing UIAlertController on iOS 14+
* Fix Error Log not showing UIAlertController on iOS <=13
* Fix wrong color in AuthenticationViewController
* Fix typo
* Fixes logging non-AltServerErrors as AltServerError.underlyingError
* Limits quitting other AltStore/SideStore processes to database migrations
* Skips logging cancelled errors
* Replaces StoreApp.latestVersion with latestSupportedVersion + latestAvailableVersion
We now store the latest supported version as a relationship on StoreApp, rather than the latest available version. This allows us to reference the latest supported version in predicates and sort descriptors.
However, we kept the underlying Core Data property name the same to avoid extra migration.
* Conforms OperatingSystemVersion to Comparable
* Parses AppVersion.minOSVersion/maxOSVersion from source JSON
* Supports non-NSManagedObjects for @Managed properties
This allows us to use @Managed with properties that may or may not be NSManagedObjects at runtime (e.g. protocols). If they are, Managed will keep strong reference to context like before.
* Supports optional @Managed properties
* Conforms AppVersion to AppProtocol
* Verifies min/max OS version before downloading app + asks user to download older app version if necessary
* Improves error message when file does not exist at AppVersion.downloadURL
* Removes unnecessary StoreApp convenience properties
* Removes unnecessary StoreApp convenience properties as well as fix other issues
* Remove Settings bundle, add SwiftUI view instead
Fix refresh all shortcut intent
* Update AuthenticationOperation.swift
Signed-off-by: June Park <rjp2030@outlook.com>
* Fix build issues given by develop
* Add availability check to fix CI build(?)
* If it's gonna be that way...
---------
Signed-off-by: June Park <rjp2030@outlook.com>
Co-authored-by: nythepegasus <nythepegasus84@gmail.com>
Co-authored-by: Riley Testut <riley@rileytestut.com>
Co-authored-by: ny <me@nythepegas.us>
433 lines
18 KiB
Swift
433 lines
18 KiB
Swift
//
|
|
// AppDelegate.swift
|
|
// AltStore
|
|
//
|
|
// Created by Riley Testut on 5/9/19.
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import UserNotifications
|
|
import AVFoundation
|
|
import Intents
|
|
|
|
import AltStoreCore
|
|
import AltSign
|
|
import Roxas
|
|
import EmotionalDamage
|
|
|
|
extension AppDelegate
|
|
{
|
|
static let openPatreonSettingsDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".OpenPatreonSettingsDeepLinkNotification")
|
|
static let importAppDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".ImportAppDeepLinkNotification")
|
|
static let addSourceDeepLinkNotification = Notification.Name(Bundle.Info.appbundleIdentifier + ".AddSourceDeepLinkNotification")
|
|
|
|
static let appBackupDidFinish = Notification.Name(Bundle.Info.appbundleIdentifier + ".AppBackupDidFinish")
|
|
|
|
static let importAppDeepLinkURLKey = "fileURL"
|
|
static let appBackupResultKey = "result"
|
|
static let addSourceDeepLinkURLKey = "sourceURL"
|
|
}
|
|
|
|
@UIApplicationMain
|
|
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
|
|
var window: UIWindow?
|
|
|
|
@available(iOS 14, *)
|
|
private var intentHandler: IntentHandler {
|
|
get { _intentHandler as! IntentHandler }
|
|
set { _intentHandler = newValue }
|
|
}
|
|
|
|
@available(iOS 14, *)
|
|
private var viewAppIntentHandler: ViewAppIntentHandler {
|
|
get { _viewAppIntentHandler as! ViewAppIntentHandler }
|
|
set { _viewAppIntentHandler = newValue }
|
|
}
|
|
|
|
private lazy var _intentHandler: Any = {
|
|
guard #available(iOS 14, *) else { fatalError() }
|
|
return IntentHandler()
|
|
}()
|
|
|
|
private lazy var _viewAppIntentHandler: Any = {
|
|
guard #available(iOS 14, *) else { fatalError() }
|
|
return ViewAppIntentHandler()
|
|
}()
|
|
|
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
|
{
|
|
// Register default settings before doing anything else.
|
|
UserDefaults.registerDefaults()
|
|
|
|
|
|
|
|
DatabaseManager.shared.start { (error) in
|
|
if let error = error
|
|
{
|
|
print("Failed to start DatabaseManager. Error:", error as Any)
|
|
}
|
|
else
|
|
{
|
|
print("Started DatabaseManager.")
|
|
}
|
|
}
|
|
|
|
AnalyticsManager.shared.start()
|
|
|
|
self.setTintColor()
|
|
|
|
SecureValueTransformer.register()
|
|
|
|
if UserDefaults.standard.firstLaunch == nil
|
|
{
|
|
Keychain.shared.reset()
|
|
UserDefaults.standard.firstLaunch = Date()
|
|
}
|
|
|
|
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
|
|
|
|
#if DEBUG || BETA
|
|
UserDefaults.standard.isDebugModeEnabled = true
|
|
#endif
|
|
|
|
self.prepareForBackgroundFetch()
|
|
|
|
return true
|
|
}
|
|
|
|
func applicationDidEnterBackground(_ application: UIApplication)
|
|
{
|
|
// Make sure to update SceneDelegate.sceneDidEnterBackground() as well.
|
|
|
|
guard let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else { return }
|
|
|
|
let midnightOneMonthAgo = Calendar.current.startOfDay(for: oneMonthAgo)
|
|
DatabaseManager.shared.purgeLoggedErrors(before: midnightOneMonthAgo) { result in
|
|
switch result
|
|
{
|
|
case .success: break
|
|
case .failure(let error): print("[ALTLog] Failed to purge logged errors before \(midnightOneMonthAgo).", error)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func applicationWillEnterForeground(_ application: UIApplication)
|
|
{
|
|
AppManager.shared.update()
|
|
start_em_proxy(bind_addr: Consts.Proxy.serverURL)
|
|
}
|
|
|
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
|
{
|
|
return self.open(url)
|
|
}
|
|
|
|
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
|
|
{
|
|
guard #available(iOS 14, *) else { return nil }
|
|
|
|
switch intent
|
|
{
|
|
case is RefreshAllIntent: return self.intentHandler
|
|
case is ViewAppIntent: return self.viewAppIntentHandler
|
|
default: return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOS 13, *)
|
|
extension AppDelegate
|
|
{
|
|
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
|
|
{
|
|
// Called when a new scene session is being created.
|
|
// Use this method to select a configuration to create the new scene with.
|
|
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
|
}
|
|
|
|
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
|
|
{
|
|
// Called when the user discards a scene session.
|
|
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
|
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
|
}
|
|
}
|
|
|
|
private extension AppDelegate
|
|
{
|
|
func setTintColor()
|
|
{
|
|
self.window?.tintColor = .altPrimary
|
|
}
|
|
|
|
func open(_ url: URL) -> Bool
|
|
{
|
|
if url.isFileURL
|
|
{
|
|
guard url.pathExtension.lowercased() == "ipa" else { return false }
|
|
|
|
DispatchQueue.main.async {
|
|
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: url])
|
|
}
|
|
|
|
return true
|
|
}
|
|
else
|
|
{
|
|
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
|
guard let host = components.host?.lowercased() else { return false }
|
|
|
|
switch host
|
|
{
|
|
case "patreon":
|
|
DispatchQueue.main.async {
|
|
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
|
}
|
|
|
|
return true
|
|
|
|
case "appbackupresponse":
|
|
let result: Result<Void, Error>
|
|
|
|
switch url.path.lowercased()
|
|
{
|
|
case "/success": result = .success(())
|
|
case "/failure":
|
|
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
|
|
guard
|
|
let errorDomain = queryItems["errorDomain"],
|
|
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
|
|
let errorDescription = queryItems["errorDescription"]
|
|
else { return false }
|
|
|
|
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
|
|
result = .failure(error)
|
|
|
|
default: return false
|
|
}
|
|
|
|
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
|
|
|
|
return true
|
|
|
|
case "install":
|
|
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
|
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
|
|
|
|
DispatchQueue.main.async {
|
|
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
|
|
}
|
|
|
|
return true
|
|
|
|
case "source":
|
|
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
|
guard let sourceURLString = queryItems["url"], let sourceURL = URL(string: sourceURLString) else { return false }
|
|
|
|
DispatchQueue.main.async {
|
|
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
|
|
}
|
|
|
|
return true
|
|
|
|
default: return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension AppDelegate
|
|
{
|
|
private func prepareForBackgroundFetch()
|
|
{
|
|
// "Fetch" every hour, but then refresh only those that need to be refreshed (so we don't drain the battery).
|
|
UIApplication.shared.setMinimumBackgroundFetchInterval(1 * 60 * 60)
|
|
|
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
|
|
}
|
|
|
|
#if DEBUG
|
|
UIApplication.shared.registerForRemoteNotifications()
|
|
#endif
|
|
}
|
|
|
|
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
|
{
|
|
let tokenParts = deviceToken.map { data -> String in
|
|
return String(format: "%02.2hhx", data)
|
|
}
|
|
|
|
let token = tokenParts.joined()
|
|
print("Push Token:", token)
|
|
}
|
|
|
|
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
|
{
|
|
self.application(application, performFetchWithCompletionHandler: completionHandler)
|
|
}
|
|
|
|
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
|
{
|
|
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
|
|
{
|
|
let threeHours: TimeInterval = 3 * 60 * 60
|
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
|
|
|
let content = UNMutableNotificationContent()
|
|
content.title = NSLocalizedString("App Refresh Tip", comment: "")
|
|
content.body = NSLocalizedString("The more you open SideStore, the more chances it's given to refresh apps in the background.", comment: "")
|
|
|
|
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
|
|
UNUserNotificationCenter.current().add(request)
|
|
|
|
UserDefaults.standard.presentedLaunchReminderNotification = true
|
|
}
|
|
|
|
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
|
|
if let error = taskResult.error
|
|
{
|
|
print("Error starting extended background task. Aborting.", error)
|
|
backgroundFetchCompletionHandler(.failed)
|
|
taskCompletionHandler()
|
|
return
|
|
}
|
|
|
|
if !DatabaseManager.shared.isStarted
|
|
{
|
|
DatabaseManager.shared.start() { (error) in
|
|
if error != nil
|
|
{
|
|
backgroundFetchCompletionHandler(.failed)
|
|
taskCompletionHandler()
|
|
}
|
|
else
|
|
{
|
|
self.performBackgroundFetch { (backgroundFetchResult) in
|
|
backgroundFetchCompletionHandler(backgroundFetchResult)
|
|
} refreshAppsCompletionHandler: { (refreshAppsResult) in
|
|
taskCompletionHandler()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self.performBackgroundFetch { (backgroundFetchResult) in
|
|
backgroundFetchCompletionHandler(backgroundFetchResult)
|
|
} refreshAppsCompletionHandler: { (refreshAppsResult) in
|
|
taskCompletionHandler()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func performBackgroundFetch(backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
|
refreshAppsCompletionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
|
{
|
|
self.fetchSources { (result) in
|
|
switch result
|
|
{
|
|
case .failure: backgroundFetchCompletionHandler(.failed)
|
|
case .success: backgroundFetchCompletionHandler(.newData)
|
|
}
|
|
|
|
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
|
{
|
|
refreshAppsCompletionHandler(.success([:]))
|
|
}
|
|
}
|
|
|
|
guard UserDefaults.standard.isBackgroundRefreshEnabled else { return }
|
|
|
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
|
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
|
AppManager.shared.backgroundRefresh(installedApps, completionHandler: refreshAppsCompletionHandler)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension AppDelegate
|
|
{
|
|
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
|
|
{
|
|
AppManager.shared.fetchSources() { (result) in
|
|
do
|
|
{
|
|
let (sources, context) = try result.get()
|
|
|
|
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
|
previousUpdatesFetchRequest.includesPendingChanges = false
|
|
previousUpdatesFetchRequest.resultType = .dictionaryResultType
|
|
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier)]
|
|
|
|
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
|
|
previousNewsItemsFetchRequest.includesPendingChanges = false
|
|
previousNewsItemsFetchRequest.resultType = .dictionaryResultType
|
|
previousNewsItemsFetchRequest.propertiesToFetch = [#keyPath(NewsItem.identifier)]
|
|
|
|
let previousUpdates = try context.fetch(previousUpdatesFetchRequest) as! [[String: String]]
|
|
let previousNewsItems = try context.fetch(previousNewsItemsFetchRequest) as! [[String: String]]
|
|
|
|
try context.save()
|
|
|
|
let updatesFetchRequest = InstalledApp.updatesFetchRequest()
|
|
let newsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
|
|
|
let updates = try context.fetch(updatesFetchRequest)
|
|
let newsItems = try context.fetch(newsItemsFetchRequest)
|
|
|
|
for update in updates
|
|
{
|
|
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
|
|
guard let storeApp = update.storeApp, let version = storeApp.latestSupportedVersion else { continue }
|
|
|
|
let content = UNMutableNotificationContent()
|
|
content.title = NSLocalizedString("New Update Available", comment: "")
|
|
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, version)
|
|
content.sound = .default
|
|
|
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
|
UNUserNotificationCenter.current().add(request)
|
|
}
|
|
|
|
for newsItem in newsItems
|
|
{
|
|
guard !previousNewsItems.contains(where: { $0[#keyPath(NewsItem.identifier)] == newsItem.identifier }) else { continue }
|
|
guard !newsItem.isSilent else { continue }
|
|
|
|
let content = UNMutableNotificationContent()
|
|
|
|
if let app = newsItem.storeApp
|
|
{
|
|
content.title = String(format: NSLocalizedString("%@ News", comment: ""), app.name)
|
|
}
|
|
else
|
|
{
|
|
content.title = NSLocalizedString("SideStore News", comment: "")
|
|
}
|
|
|
|
content.body = newsItem.title
|
|
content.sound = .default
|
|
|
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
|
|
UNUserNotificationCenter.current().add(request)
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
UIApplication.shared.applicationIconBadgeNumber = updates.count
|
|
}
|
|
|
|
completionHandler(.success(sources))
|
|
}
|
|
catch
|
|
{
|
|
print("Error fetching apps:", error)
|
|
completionHandler(.failure(error))
|
|
}
|
|
}
|
|
}
|
|
}
|