mirror of
https://github.com/SideStore/SideStore.git
synced 2026-05-16 06:15:39 +02:00
Merge branch 'module_refactoring' into develop
This commit is contained in:
@@ -4,6 +4,12 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// Shared
|
||||
#import "ALTConstants.h"
|
||||
#import "ALTConnection.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "CFNotificationName+AltStore.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AKDevice : NSObject
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
|
||||
private let ReceivedLocalServerConnectionRequest: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
|
||||
{ (center, observer, name, object, userInfo) in
|
||||
guard let name = name, let observer = observer else { return }
|
||||
|
||||
@@ -7,21 +7,20 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltKit
|
||||
|
||||
typealias ConnectionManager = AltKit.ConnectionManager<RequestHandler>
|
||||
typealias DaemonConnectionManager = ConnectionManager<DaemonRequestHandler>
|
||||
|
||||
private let connectionManager = ConnectionManager(requestHandler: RequestHandler(),
|
||||
private let connectionManager = ConnectionManager(requestHandler: DaemonRequestHandler(),
|
||||
connectionHandlers: [LocalConnectionHandler()])
|
||||
|
||||
extension ConnectionManager
|
||||
extension DaemonConnectionManager
|
||||
{
|
||||
static var shared: ConnectionManager {
|
||||
return connectionManager
|
||||
}
|
||||
}
|
||||
|
||||
struct RequestHandler: AltKit.RequestHandler
|
||||
struct DaemonRequestHandler: RequestHandler
|
||||
{
|
||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
import Foundation
|
||||
|
||||
autoreleasepool {
|
||||
ConnectionManager.shared.start()
|
||||
DaemonConnectionManager.shared.start()
|
||||
RunLoop.current.run()
|
||||
}
|
||||
|
||||
@@ -5,4 +5,9 @@
|
||||
#import "ALTDeviceManager.h"
|
||||
#import "ALTWiredConnection.h"
|
||||
#import "ALTNotificationConnection.h"
|
||||
#import "AltKit.h"
|
||||
|
||||
// Shared
|
||||
#import "ALTConstants.h"
|
||||
#import "ALTConnection.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
#import "CFNotificationName+AltStore.h"
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltKit
|
||||
|
||||
class AnisetteDataManager: NSObject
|
||||
{
|
||||
|
||||
@@ -63,7 +63,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
|
||||
ConnectionManager.shared.start()
|
||||
ServerConnectionManager.shared.start()
|
||||
ALTDeviceManager.shared.start()
|
||||
|
||||
let item = NSStatusBar.system.statusItem(withLength: -1)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <AltSign/AltSign.h>
|
||||
#import "AltSign.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
//
|
||||
|
||||
#import "ALTNotificationConnection+Private.h"
|
||||
#import "AltKit.h"
|
||||
|
||||
#import "NSError+ALTServerError.h"
|
||||
|
||||
void ALTDeviceReceivedNotification(const char *notification, void *user_data);
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <AltSign/AltSign.h>
|
||||
#import "AltSign.h"
|
||||
|
||||
#import "AltKit.h"
|
||||
#import "ALTConnection.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
//
|
||||
|
||||
#import "ALTWiredConnection+Private.h"
|
||||
#import "AltKit.h"
|
||||
|
||||
#import "ALTConnection.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
|
||||
@implementation ALTWiredConnection
|
||||
|
||||
@@ -7,21 +7,20 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltKit
|
||||
|
||||
typealias ConnectionManager = AltKit.ConnectionManager<RequestHandler>
|
||||
typealias ServerConnectionManager = ConnectionManager<ServerRequestHandler>
|
||||
|
||||
private let connectionManager = ConnectionManager(requestHandler: RequestHandler(),
|
||||
private let connectionManager = ConnectionManager(requestHandler: ServerRequestHandler(),
|
||||
connectionHandlers: [WirelessConnectionHandler(), WiredConnectionHandler()])
|
||||
|
||||
extension ConnectionManager
|
||||
extension ServerConnectionManager
|
||||
{
|
||||
static var shared: ConnectionManager {
|
||||
return connectionManager
|
||||
}
|
||||
}
|
||||
|
||||
struct RequestHandler: AltKit.RequestHandler
|
||||
struct ServerRequestHandler: RequestHandler
|
||||
{
|
||||
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
|
||||
{
|
||||
@@ -187,7 +186,7 @@ private extension RequestHandler
|
||||
let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid, activeProvisioningProfiles: activeProvisioningProfiles) { (success, error) in
|
||||
print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription)
|
||||
|
||||
if let error = error.map { ALTServerError($0) }
|
||||
if let error = error.map({ ALTServerError($0) })
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltKit
|
||||
|
||||
class WiredConnectionHandler: ConnectionHandler
|
||||
{
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
|
||||
extension WirelessConnectionHandler
|
||||
{
|
||||
public enum State
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AltSign/AltSign.h>
|
||||
#import "AltSign.h"
|
||||
|
||||
@class ALTWiredConnection;
|
||||
@class ALTNotificationConnection;
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
#import "ALTDeviceManager.h"
|
||||
|
||||
#import "AltKit.h"
|
||||
#import "ALTWiredConnection+Private.h"
|
||||
#import "ALTNotificationConnection+Private.h"
|
||||
|
||||
#import "ALTConstants.h"
|
||||
#import "NSError+ALTServerError.h"
|
||||
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
#include <libimobiledevice/lockdown.h>
|
||||
#include <libimobiledevice/installation_proxy.h>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,67 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1130"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
||||
BuildableName = "libAltKit.a"
|
||||
BlueprintName = "AltKit"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF1E314F22A0616100370A3C"
|
||||
BuildableName = "libAltKit.a"
|
||||
BlueprintName = "AltKit"
|
||||
ReferencedContainer = "container:AltStore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
2
AltStore.xcworkspace/contents.xcworkspacedata
generated
2
AltStore.xcworkspace/contents.xcworkspacedata
generated
@@ -5,7 +5,7 @@
|
||||
location = "container:AltStore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Dependencies/AltSign/AltSign.xcodeproj">
|
||||
location = "group:Dependencies/AltSign">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Dependencies/Roxas/Roxas.xcodeproj">
|
||||
|
||||
@@ -2,10 +2,4 @@
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "AltKit.h"
|
||||
|
||||
#import "ALTAppPermission.h"
|
||||
#import "ALTPatreonBenefitType.h"
|
||||
#import "ALTSourceUserInfoKey.h"
|
||||
|
||||
#import "NSAttributedString+Markdown.h"
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.siri</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.rileytestut.AltStore</string>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
import AppCenter
|
||||
import AppCenterAnalytics
|
||||
import AppCenterCrashes
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
class PermissionPopoverViewController: UIViewController
|
||||
{
|
||||
var permission: AppPermission!
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
class AppIDsViewController: UICollectionViewController
|
||||
|
||||
@@ -9,47 +9,12 @@
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import AVFoundation
|
||||
import Intents
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import AltKit
|
||||
import Roxas
|
||||
|
||||
private enum RefreshError: LocalizedError
|
||||
{
|
||||
case noInstalledApps
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CFNotificationName
|
||||
{
|
||||
static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString)
|
||||
static let appIsRunning = CFNotificationName("com.altstore.AppState.Running" as CFString)
|
||||
|
||||
static func requestAppState(for appID: String) -> CFNotificationName
|
||||
{
|
||||
let name = String(CFNotificationName.requestAppState.rawValue) + "." + appID
|
||||
return CFNotificationName(name as CFString)
|
||||
}
|
||||
|
||||
static func appIsRunning(for appID: String) -> CFNotificationName
|
||||
{
|
||||
let name = String(CFNotificationName.appIsRunning.rawValue) + "." + appID
|
||||
return CFNotificationName(name as CFString)
|
||||
}
|
||||
}
|
||||
|
||||
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
|
||||
{ (center, observer, name, object, userInfo) in
|
||||
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let name = name else { return }
|
||||
appDelegate.receivedApplicationState(notification: name)
|
||||
}
|
||||
|
||||
extension AppDelegate
|
||||
{
|
||||
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
|
||||
@@ -68,11 +33,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
private var runningApplications: Set<String>?
|
||||
private var backgroundRefreshContext: NSManagedObjectContext? // Keep context alive until finished refreshing.
|
||||
private lazy var intentHandler = IntentHandler()
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
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()
|
||||
@@ -117,6 +92,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
{
|
||||
return self.open(url)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any?
|
||||
{
|
||||
guard intent is RefreshAllIntent else { return nil }
|
||||
return self.intentHandler
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
@@ -234,93 +233,92 @@ extension AppDelegate
|
||||
|
||||
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||
{
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled && !UserDefaults.standard.presentedLaunchReminderNotification
|
||||
{
|
||||
ServerManager.shared.startDiscovering()
|
||||
let threeHours: TimeInterval = 3 * 60 * 60
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
|
||||
|
||||
if !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 AltStore, the more chances it's given to refresh apps in the background.", comment: "")
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("App Refresh Tip", comment: "")
|
||||
content.body = NSLocalizedString("The more you open AltStore, 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)
|
||||
|
||||
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
UserDefaults.standard.presentedLaunchReminderNotification = true
|
||||
}
|
||||
UserDefaults.standard.presentedLaunchReminderNotification = true
|
||||
}
|
||||
|
||||
let refreshIdentifier = UUID().uuidString
|
||||
|
||||
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
|
||||
|
||||
func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>)
|
||||
{
|
||||
// If finish is actually called, that means an error occured during installation.
|
||||
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
{
|
||||
ServerManager.shared.stopDiscovering()
|
||||
self.scheduleFinishedRefreshingNotification(for: result, identifier: refreshIdentifier, delay: 0)
|
||||
}
|
||||
|
||||
taskCompletionHandler()
|
||||
|
||||
self.backgroundRefreshContext = nil
|
||||
}
|
||||
|
||||
if let error = taskResult.error
|
||||
{
|
||||
print("Error starting extended background task. Aborting.", error)
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
finish(.failure(error))
|
||||
taskCompletionHandler()
|
||||
return
|
||||
}
|
||||
|
||||
if !DatabaseManager.shared.isStarted
|
||||
{
|
||||
DatabaseManager.shared.start() { (error) in
|
||||
if let error = error
|
||||
if error != nil
|
||||
{
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
finish(.failure(error))
|
||||
taskCompletionHandler()
|
||||
}
|
||||
else
|
||||
{
|
||||
self.refreshApps(identifier: refreshIdentifier, backgroundFetchCompletionHandler: backgroundFetchCompletionHandler, completionHandler: finish(_:))
|
||||
self.performBackgroundFetch { (backgroundFetchResult) in
|
||||
backgroundFetchCompletionHandler(backgroundFetchResult)
|
||||
} refreshAppsCompletionHandler: { (refreshAppsResult) in
|
||||
taskCompletionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.refreshApps(identifier: refreshIdentifier, backgroundFetchCompletionHandler: backgroundFetchCompletionHandler, completionHandler: finish(_:))
|
||||
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 refreshApps(identifier: String,
|
||||
backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||
completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
||||
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
|
||||
{
|
||||
var fetchSourcesResult: Result<Set<Source>, Error>?
|
||||
var serversResult: Result<Void, Error>?
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
dispatchGroup.enter()
|
||||
|
||||
AppManager.shared.fetchSources() { (result) in
|
||||
fetchSourcesResult = result.map { $0.0 }.mapError { $0 as Error }
|
||||
|
||||
do
|
||||
{
|
||||
let (_, context) = try result.get()
|
||||
let (sources, context) = try result.get()
|
||||
|
||||
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
||||
previousUpdatesFetchRequest.includesPendingChanges = false
|
||||
@@ -383,223 +381,14 @@ private extension AppDelegate
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.applicationIconBadgeNumber = updates.count
|
||||
}
|
||||
|
||||
completionHandler(.success(sources))
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Error fetching apps:", error)
|
||||
|
||||
fetchSourcesResult = .failure(error)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
if UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
||||
guard !installedApps.isEmpty else {
|
||||
serversResult = .success(())
|
||||
dispatchGroup.leave()
|
||||
|
||||
completionHandler(.failure(RefreshError.noInstalledApps))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.runningApplications = []
|
||||
self.backgroundRefreshContext = context
|
||||
|
||||
let identifiers = installedApps.compactMap { $0.bundleIdentifier }
|
||||
print("Apps to refresh:", identifiers)
|
||||
|
||||
DispatchQueue.global().async {
|
||||
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
|
||||
for identifier in identifiers
|
||||
{
|
||||
let appIsRunningNotification = CFNotificationName.appIsRunning(for: identifier)
|
||||
CFNotificationCenterAddObserver(notificationCenter, nil, ReceivedApplicationState, appIsRunningNotification.rawValue, nil, .deliverImmediately)
|
||||
|
||||
let requestAppStateNotification = CFNotificationName.requestAppState(for: identifier)
|
||||
CFNotificationCenterPostNotification(notificationCenter, requestAppStateNotification, nil, nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for three seconds to:
|
||||
// a) give us time to discover AltServers
|
||||
// b) give other processes a chance to respond to requestAppState notification
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
context.perform {
|
||||
if ServerManager.shared.discoveredServers.isEmpty
|
||||
{
|
||||
serversResult = .failure(ConnectionError.serverNotFound)
|
||||
}
|
||||
else
|
||||
{
|
||||
serversResult = .success(())
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
|
||||
let filteredApps = installedApps.filter { !(self.runningApplications?.contains($0.bundleIdentifier) ?? false) }
|
||||
print("Filtered Apps to Refresh:", filteredApps.map { $0.bundleIdentifier })
|
||||
|
||||
let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil)
|
||||
group.beginInstallationHandler = { (installedApp) in
|
||||
guard installedApp.bundleIdentifier == StoreApp.altstoreAppID else { return }
|
||||
|
||||
// We're starting to install AltStore, which means the app is about to quit.
|
||||
// So, we schedule a "refresh successful" local notification to be displayed after a delay,
|
||||
// but if the app is still running, we cancel the notification.
|
||||
// Then, we schedule another notification and repeat the process.
|
||||
|
||||
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background.
|
||||
|
||||
if let error = group.context.error
|
||||
{
|
||||
self.scheduleFinishedRefreshingNotification(for: .failure(error), identifier: identifier)
|
||||
}
|
||||
else
|
||||
{
|
||||
var results = group.results
|
||||
results[installedApp.bundleIdentifier] = .success(installedApp)
|
||||
|
||||
self.scheduleFinishedRefreshingNotification(for: .success(results), identifier: identifier)
|
||||
}
|
||||
}
|
||||
group.completionHandler = { (results) in
|
||||
completionHandler(.success(results))
|
||||
}
|
||||
}
|
||||
}
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
{
|
||||
guard let fetchSourcesResult = fetchSourcesResult else {
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
switch fetchSourcesResult
|
||||
{
|
||||
case .failure: backgroundFetchCompletionHandler(.failed)
|
||||
case .success: backgroundFetchCompletionHandler(.newData)
|
||||
}
|
||||
|
||||
completionHandler(.success([:]))
|
||||
}
|
||||
else
|
||||
{
|
||||
guard let fetchSourcesResult = fetchSourcesResult, let serversResult = serversResult else {
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
// Call completionHandler early to improve chances of refreshing in the background again.
|
||||
switch (fetchSourcesResult, serversResult)
|
||||
{
|
||||
case (.success, .success): backgroundFetchCompletionHandler(.newData)
|
||||
case (.success, .failure(ConnectionError.serverNotFound)): backgroundFetchCompletionHandler(.newData)
|
||||
case (.failure, _), (_, .failure): backgroundFetchCompletionHandler(.failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func receivedApplicationState(notification: CFNotificationName)
|
||||
{
|
||||
let baseName = String(CFNotificationName.appIsRunning.rawValue)
|
||||
|
||||
let appID = String(notification.rawValue).replacingOccurrences(of: baseName + ".", with: "")
|
||||
self.runningApplications?.insert(appID)
|
||||
}
|
||||
|
||||
func scheduleFinishedRefreshingNotification(for result: Result<[String: Result<InstalledApp, Error>], Error>, identifier: String, delay: TimeInterval = 5)
|
||||
{
|
||||
func scheduleFinishedRefreshingNotification()
|
||||
{
|
||||
self.cancelFinishedRefreshingNotification(identifier: identifier)
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
var shouldPresentAlert = true
|
||||
|
||||
do
|
||||
{
|
||||
let results = try result.get()
|
||||
shouldPresentAlert = !results.isEmpty
|
||||
|
||||
for (_, result) in results
|
||||
{
|
||||
guard case let .failure(error) = result else { continue }
|
||||
throw error
|
||||
}
|
||||
|
||||
content.title = NSLocalizedString("Refreshed Apps", comment: "")
|
||||
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
|
||||
}
|
||||
catch ConnectionError.serverNotFound
|
||||
{
|
||||
shouldPresentAlert = false
|
||||
}
|
||||
catch RefreshError.noInstalledApps
|
||||
{
|
||||
shouldPresentAlert = false
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to refresh apps in background.", error)
|
||||
|
||||
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
||||
content.body = error.localizedDescription
|
||||
|
||||
shouldPresentAlert = true
|
||||
}
|
||||
|
||||
if shouldPresentAlert
|
||||
{
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
|
||||
|
||||
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
if delay > 0
|
||||
{
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests() { (requests) in
|
||||
// If app is still running at this point, we schedule another notification with same identifier.
|
||||
// This prevents the currently scheduled notification from displaying, and starts another countdown timer.
|
||||
// First though, make sure there _is_ still a pending request, otherwise it's been cancelled
|
||||
// and we should stop polling.
|
||||
guard requests.contains(where: { $0.identifier == identifier }) else { return }
|
||||
|
||||
scheduleFinishedRefreshingNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scheduleFinishedRefreshingNotification()
|
||||
|
||||
// Perform synchronously to ensure app doesn't quit before we've finishing saving to disk.
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
_ = RefreshAttempt(identifier: identifier, result: result, context: context)
|
||||
|
||||
do { try context.save() }
|
||||
catch { print("Failed to save refresh attempt.", error) }
|
||||
}
|
||||
}
|
||||
|
||||
func cancelFinishedRefreshingNotification(identifier: String)
|
||||
{
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AltSign
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
class RefreshAltStoreViewController: UIViewController
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
@@ -83,6 +84,7 @@ private extension BrowseViewController
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
cell.subtitleLabel.text = app.subtitle
|
||||
cell.imageURLs = Array(app.screenshotURLs.prefix(2))
|
||||
|
||||
cell.bannerView.configure(for: app)
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
class AppBannerView: RSTNibView
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension TimeInterval
|
||||
{
|
||||
static let shortToastViewDuration = 4.0
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltKit
|
||||
import AltStoreCore
|
||||
|
||||
extension FileManager
|
||||
{
|
||||
|
||||
23
AltStore/Extensions/INInteraction+AltStore.swift
Normal file
23
AltStore/Extensions/INInteraction+AltStore.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// INInteraction+AltStore.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 9/4/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Intents
|
||||
|
||||
// Requires iOS 14 in-app intent handling.
|
||||
@available(iOS 14, *)
|
||||
extension INInteraction
|
||||
{
|
||||
static func refreshAllApps() -> INInteraction
|
||||
{
|
||||
let refreshAllIntent = RefreshAllIntent()
|
||||
refreshAllIntent.suggestedInvocationPhrase = NSString.deferredLocalizedIntentsString(with: "Refresh my apps") as String
|
||||
|
||||
let interaction = INInteraction(intent: refreshAllIntent, response: nil)
|
||||
return interaction
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,10 @@
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>INIntentsSupported</key>
|
||||
<array>
|
||||
<string>RefreshAllIntent</string>
|
||||
</array>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>altstore-com.rileytestut.AltStore</string>
|
||||
@@ -85,6 +89,31 @@
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>AltStore uses the local network to find and communicate with AltServer.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>RefreshAllIntent</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<true/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
|
||||
119
AltStore/Intents/IntentHandler.swift
Normal file
119
AltStore/Intents/IntentHandler.swift
Normal file
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// IntentHandler.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/6/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
class IntentHandler: NSObject, RefreshAllIntentHandling
|
||||
{
|
||||
private let queue = DispatchQueue(label: "io.altstore.IntentHandler")
|
||||
|
||||
private var completionHandlers = [RefreshAllIntent: (RefreshAllIntentResponse) -> Void]()
|
||||
private var queuedResponses = [RefreshAllIntent: RefreshAllIntentResponse]()
|
||||
|
||||
func confirm(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
|
||||
{
|
||||
// Refreshing apps usually, but not always, completes within alotted time.
|
||||
// As a workaround, we'll start refreshing apps in confirm() so we can
|
||||
// take advantage of some extra time before starting handle() timeout timer.
|
||||
|
||||
self.completionHandlers[intent] = { (response) in
|
||||
if response.code != .ready
|
||||
{
|
||||
// Operation finished before confirmation "timeout".
|
||||
// Cache response to return it when handle() is called.
|
||||
self.queuedResponses[intent] = response
|
||||
}
|
||||
|
||||
completion(RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
||||
}
|
||||
|
||||
// Give ourselves 9 extra seconds before starting handle() timeout timer.
|
||||
// 10 seconds or longer results in timeout regardless.
|
||||
self.queue.asyncAfter(deadline: .now() + 9.0) {
|
||||
self.finish(intent, response: RefreshAllIntentResponse(code: .ready, userActivity: nil))
|
||||
}
|
||||
|
||||
if !DatabaseManager.shared.isStarted
|
||||
{
|
||||
DatabaseManager.shared.start() { (error) in
|
||||
if let error = error
|
||||
{
|
||||
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedDescription))
|
||||
}
|
||||
else
|
||||
{
|
||||
self.refreshApps(intent: intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.refreshApps(intent: intent)
|
||||
}
|
||||
}
|
||||
|
||||
func handle(intent: RefreshAllIntent, completion: @escaping (RefreshAllIntentResponse) -> Void)
|
||||
{
|
||||
self.completionHandlers[intent] = { (response) in
|
||||
// Ignore .ready response from confirm() timeout.
|
||||
guard response.code != .ready else { return }
|
||||
completion(response)
|
||||
}
|
||||
|
||||
if let response = self.queuedResponses[intent]
|
||||
{
|
||||
self.queuedResponses[intent] = nil
|
||||
self.finish(intent, response: response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension IntentHandler
|
||||
{
|
||||
func finish(_ intent: RefreshAllIntent, response: RefreshAllIntentResponse)
|
||||
{
|
||||
self.queue.async {
|
||||
guard let completionHandler = self.completionHandlers[intent] else { return }
|
||||
self.completionHandlers[intent] = nil
|
||||
|
||||
completionHandler(response)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshApps(intent: RefreshAllIntent)
|
||||
{
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let installedApps = InstalledApp.fetchActiveApps(in: context)
|
||||
AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { (result) in
|
||||
do
|
||||
{
|
||||
let results = try result.get()
|
||||
|
||||
for (_, result) in results
|
||||
{
|
||||
guard case let .failure(error) = result else { continue }
|
||||
throw error
|
||||
}
|
||||
|
||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||
}
|
||||
catch RefreshError.noInstalledApps
|
||||
{
|
||||
self.finish(intent, response: RefreshAllIntentResponse(code: .success, userActivity: nil))
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
print("Failed to refresh apps in background.", error)
|
||||
self.finish(intent, response: RefreshAllIntentResponse.failure(localizedDescription: error.localizedFailureReason ?? error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
AltStore/Intents/Intents.intentdefinition
Normal file
120
AltStore/Intents/Intents.intentdefinition
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>INEnums</key>
|
||||
<array/>
|
||||
<key>INIntentDefinitionModelVersion</key>
|
||||
<string>1.2</string>
|
||||
<key>INIntentDefinitionNamespace</key>
|
||||
<string>KyhEWE</string>
|
||||
<key>INIntentDefinitionSystemVersion</key>
|
||||
<string>20A5354i</string>
|
||||
<key>INIntentDefinitionToolsBuildVersion</key>
|
||||
<string>12A8189n</string>
|
||||
<key>INIntentDefinitionToolsVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>INIntents</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentCategory</key>
|
||||
<string>generic</string>
|
||||
<key>INIntentConfigurable</key>
|
||||
<true/>
|
||||
<key>INIntentDescriptionID</key>
|
||||
<string>62S1rm</string>
|
||||
<key>INIntentLastParameterTag</key>
|
||||
<integer>3</integer>
|
||||
<key>INIntentManagedParameterCombinations</key>
|
||||
<dict>
|
||||
<key></key>
|
||||
<dict>
|
||||
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
|
||||
<true/>
|
||||
<key>INIntentParameterCombinationTitle</key>
|
||||
<string>Refresh All Apps</string>
|
||||
<key>INIntentParameterCombinationTitleID</key>
|
||||
<string>cJxa2I</string>
|
||||
<key>INIntentParameterCombinationUpdatesLinked</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>INIntentName</key>
|
||||
<string>RefreshAll</string>
|
||||
<key>INIntentParameterCombinations</key>
|
||||
<dict>
|
||||
<key></key>
|
||||
<dict>
|
||||
<key>INIntentParameterCombinationSupportsBackgroundExecution</key>
|
||||
<true/>
|
||||
<key>INIntentParameterCombinationTitle</key>
|
||||
<string>Refresh All Apps</string>
|
||||
<key>INIntentParameterCombinationTitleID</key>
|
||||
<string>DKTGdO</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>INIntentResponse</key>
|
||||
<dict>
|
||||
<key>INIntentResponseCodes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentResponseCodeConciseFormatString</key>
|
||||
<string>All apps have been refreshed.</string>
|
||||
<key>INIntentResponseCodeConciseFormatStringID</key>
|
||||
<string>3WMWsJ</string>
|
||||
<key>INIntentResponseCodeFormatString</key>
|
||||
<string>All apps have been refreshed.</string>
|
||||
<key>INIntentResponseCodeFormatStringID</key>
|
||||
<string>BjInD3</string>
|
||||
<key>INIntentResponseCodeName</key>
|
||||
<string>success</string>
|
||||
<key>INIntentResponseCodeSuccess</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>INIntentResponseCodeConciseFormatString</key>
|
||||
<string>${localizedDescription}</string>
|
||||
<key>INIntentResponseCodeConciseFormatStringID</key>
|
||||
<string>GJdShK</string>
|
||||
<key>INIntentResponseCodeFormatString</key>
|
||||
<string>${localizedDescription}</string>
|
||||
<key>INIntentResponseCodeFormatStringID</key>
|
||||
<string>oXAiOU</string>
|
||||
<key>INIntentResponseCodeName</key>
|
||||
<string>failure</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>INIntentResponseLastParameterTag</key>
|
||||
<integer>3</integer>
|
||||
<key>INIntentResponseParameters</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentResponseParameterDisplayName</key>
|
||||
<string>Localized Description</string>
|
||||
<key>INIntentResponseParameterDisplayNameID</key>
|
||||
<string>wdy22v</string>
|
||||
<key>INIntentResponseParameterDisplayPriority</key>
|
||||
<integer>1</integer>
|
||||
<key>INIntentResponseParameterName</key>
|
||||
<string>localizedDescription</string>
|
||||
<key>INIntentResponseParameterTag</key>
|
||||
<integer>3</integer>
|
||||
<key>INIntentResponseParameterType</key>
|
||||
<string>String</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>INIntentTitle</key>
|
||||
<string>Refresh All Apps</string>
|
||||
<key>INIntentTitleID</key>
|
||||
<string>2b6Xto</string>
|
||||
<key>INIntentType</key>
|
||||
<string>Custom</string>
|
||||
<key>INIntentVerb</key>
|
||||
<string>Do</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>INTypes</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -9,6 +9,8 @@
|
||||
import UIKit
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
class LaunchViewController: RSTLaunchViewController
|
||||
{
|
||||
private var didFinishLaunching = false
|
||||
|
||||
@@ -10,10 +10,11 @@ import Foundation
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import MobileCoreServices
|
||||
import Intents
|
||||
import Combine
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import AltKit
|
||||
|
||||
import Roxas
|
||||
|
||||
extension AppManager
|
||||
@@ -23,15 +24,41 @@ extension AppManager
|
||||
static let expirationWarningNotificationID = "altstore-expiration-warning"
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
class AppManagerPublisher: ObservableObject
|
||||
{
|
||||
@Published
|
||||
fileprivate(set) var installationProgress = [String: Progress]()
|
||||
|
||||
@Published
|
||||
fileprivate(set) var refreshProgress = [String: Progress]()
|
||||
}
|
||||
|
||||
class AppManager
|
||||
{
|
||||
static let shared = AppManager()
|
||||
|
||||
@available(iOS 13, *)
|
||||
private(set) lazy var publisher: AppManagerPublisher = AppManagerPublisher()
|
||||
|
||||
private let operationQueue = OperationQueue()
|
||||
private let serialOperationQueue = OperationQueue()
|
||||
|
||||
private var installationProgress = [String: Progress]()
|
||||
private var refreshProgress = [String: Progress]()
|
||||
private var installationProgress = [String: Progress]() {
|
||||
didSet {
|
||||
guard #available(iOS 13, *) else { return }
|
||||
self.publisher.installationProgress = self.installationProgress
|
||||
}
|
||||
}
|
||||
private var refreshProgress = [String: Progress]() {
|
||||
didSet {
|
||||
guard #available(iOS 13, *) else { return }
|
||||
self.publisher.refreshProgress = self.refreshProgress
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
private lazy var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private init()
|
||||
{
|
||||
@@ -39,6 +66,28 @@ class AppManager
|
||||
|
||||
self.serialOperationQueue.name = "com.altstore.AppManager.serialOperationQueue"
|
||||
self.serialOperationQueue.maxConcurrentOperationCount = 1
|
||||
|
||||
if #available(iOS 13, *)
|
||||
{
|
||||
self.prepareSubscriptions()
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
func prepareSubscriptions()
|
||||
{
|
||||
self.publisher.$refreshProgress
|
||||
.receive(on: RunLoop.main)
|
||||
.map(\.keys)
|
||||
.flatMap { (bundleIDs) in
|
||||
DatabaseManager.shared.viewContext.registeredObjects.publisher
|
||||
.compactMap { $0 as? InstalledApp }
|
||||
.map { ($0, bundleIDs) }
|
||||
}
|
||||
.sink { (installedApp, bundleIDs) in
|
||||
installedApp.isRefreshing = bundleIDs.contains(installedApp.bundleIdentifier)
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,6 +549,17 @@ extension AppManager
|
||||
}
|
||||
}
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
func backgroundRefresh(_ installedApps: [InstalledApp], presentsNotifications: Bool = true, completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
||||
{
|
||||
let backgroundRefreshAppsOperation = BackgroundRefreshAppsOperation(installedApps: installedApps)
|
||||
backgroundRefreshAppsOperation.resultHandler = completionHandler
|
||||
backgroundRefreshAppsOperation.presentsFinishedNotification = presentsNotifications
|
||||
self.run([backgroundRefreshAppsOperation], context: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppManager
|
||||
{
|
||||
enum AppOperation
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
struct FetchSourcesError: LocalizedError, CustomNSError
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import Intents
|
||||
|
||||
import AltKit
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
@@ -654,6 +654,15 @@ private extension MyAppsViewController
|
||||
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 14, *)
|
||||
{
|
||||
let interaction = INInteraction.refreshAllApps()
|
||||
interaction.donate { (error) in
|
||||
guard let error = error else { return }
|
||||
print("Failed to donate intent \(interaction.intent).", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func updateApp(_ sender: UIButton)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import UIKit
|
||||
import SafariServices
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import Roxas
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
enum AuthenticationError: LocalizedError
|
||||
|
||||
272
AltStore/Operations/BackgroundRefreshAppsOperation.swift
Normal file
272
AltStore/Operations/BackgroundRefreshAppsOperation.swift
Normal file
@@ -0,0 +1,272 @@
|
||||
//
|
||||
// BackgroundRefreshAppsOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/6/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
enum RefreshError: LocalizedError
|
||||
{
|
||||
case noInstalledApps
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .noInstalledApps: return NSLocalizedString("No active apps require refreshing.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CFNotificationName
|
||||
{
|
||||
static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString)
|
||||
static let appIsRunning = CFNotificationName("com.altstore.AppState.Running" as CFString)
|
||||
|
||||
static func requestAppState(for appID: String) -> CFNotificationName
|
||||
{
|
||||
let name = String(CFNotificationName.requestAppState.rawValue) + "." + appID
|
||||
return CFNotificationName(name as CFString)
|
||||
}
|
||||
|
||||
static func appIsRunning(for appID: String) -> CFNotificationName
|
||||
{
|
||||
let name = String(CFNotificationName.appIsRunning.rawValue) + "." + appID
|
||||
return CFNotificationName(name as CFString)
|
||||
}
|
||||
}
|
||||
|
||||
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
|
||||
{ (center, observer, name, object, userInfo) in
|
||||
guard let name = name, let observer = observer else { return }
|
||||
|
||||
let operation = unsafeBitCast(observer, to: BackgroundRefreshAppsOperation.self)
|
||||
operation.receivedApplicationState(notification: name)
|
||||
}
|
||||
|
||||
@objc(BackgroundRefreshAppsOperation)
|
||||
class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<InstalledApp, Error>]>
|
||||
{
|
||||
let installedApps: [InstalledApp]
|
||||
private let managedObjectContext: NSManagedObjectContext
|
||||
|
||||
var presentsFinishedNotification: Bool = true
|
||||
|
||||
private let refreshIdentifier: String = UUID().uuidString
|
||||
private var runningApplications: Set<String> = []
|
||||
|
||||
init(installedApps: [InstalledApp])
|
||||
{
|
||||
self.installedApps = installedApps
|
||||
self.managedObjectContext = installedApps.compactMap({ $0.managedObjectContext }).first ?? DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>)
|
||||
{
|
||||
super.finish(result)
|
||||
|
||||
self.scheduleFinishedRefreshingNotification(for: result, delay: 0)
|
||||
|
||||
self.managedObjectContext.perform {
|
||||
self.stopListeningForRunningApps()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if UIApplication.shared.applicationState == .background
|
||||
{
|
||||
ServerManager.shared.stopDiscovering()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
guard !self.installedApps.isEmpty else {
|
||||
self.finish(.failure(RefreshError.noInstalledApps))
|
||||
return
|
||||
}
|
||||
|
||||
if !ServerManager.shared.isDiscovering
|
||||
{
|
||||
ServerManager.shared.startDiscovering()
|
||||
}
|
||||
|
||||
self.managedObjectContext.perform {
|
||||
print("Apps to refresh:", self.installedApps.map(\.bundleIdentifier))
|
||||
|
||||
self.startListeningForRunningApps()
|
||||
|
||||
// Wait for three seconds to:
|
||||
// a) give us time to discover AltServers
|
||||
// b) give other processes a chance to respond to requestAppState notification
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
self.managedObjectContext.perform {
|
||||
guard !ServerManager.shared.discoveredServers.isEmpty else { return self.finish(.failure(ConnectionError.serverNotFound)) }
|
||||
|
||||
let filteredApps = self.installedApps.filter { !self.runningApplications.contains($0.bundleIdentifier) }
|
||||
print("Filtered Apps to Refresh:", filteredApps.map { $0.bundleIdentifier })
|
||||
|
||||
let group = AppManager.shared.refresh(filteredApps, presentingViewController: nil)
|
||||
group.beginInstallationHandler = { (installedApp) in
|
||||
guard installedApp.bundleIdentifier == StoreApp.altstoreAppID else { return }
|
||||
|
||||
// We're starting to install AltStore, which means the app is about to quit.
|
||||
// So, we schedule a "refresh successful" local notification to be displayed after a delay,
|
||||
// but if the app is still running, we cancel the notification.
|
||||
// Then, we schedule another notification and repeat the process.
|
||||
|
||||
// Also since AltServer has already received the app, it can finish installing even if we're no longer running in background.
|
||||
|
||||
if let error = group.context.error
|
||||
{
|
||||
self.scheduleFinishedRefreshingNotification(for: .failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
var results = group.results
|
||||
results[installedApp.bundleIdentifier] = .success(installedApp)
|
||||
|
||||
self.scheduleFinishedRefreshingNotification(for: .success(results))
|
||||
}
|
||||
}
|
||||
group.completionHandler = { (results) in
|
||||
self.finish(.success(results))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension BackgroundRefreshAppsOperation
|
||||
{
|
||||
func startListeningForRunningApps()
|
||||
{
|
||||
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
let observer = Unmanaged.passUnretained(self).toOpaque()
|
||||
|
||||
for installedApp in self.installedApps
|
||||
{
|
||||
let appIsRunningNotification = CFNotificationName.appIsRunning(for: installedApp.bundleIdentifier)
|
||||
CFNotificationCenterAddObserver(notificationCenter, observer, ReceivedApplicationState, appIsRunningNotification.rawValue, nil, .deliverImmediately)
|
||||
|
||||
let requestAppStateNotification = CFNotificationName.requestAppState(for: installedApp.bundleIdentifier)
|
||||
CFNotificationCenterPostNotification(notificationCenter, requestAppStateNotification, nil, nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
func stopListeningForRunningApps()
|
||||
{
|
||||
let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
let observer = Unmanaged.passUnretained(self).toOpaque()
|
||||
|
||||
for installedApp in self.installedApps
|
||||
{
|
||||
let appIsRunningNotification = CFNotificationName.appIsRunning(for: installedApp.bundleIdentifier)
|
||||
CFNotificationCenterRemoveObserver(notificationCenter, observer, appIsRunningNotification, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func receivedApplicationState(notification: CFNotificationName)
|
||||
{
|
||||
let baseName = String(CFNotificationName.appIsRunning.rawValue)
|
||||
|
||||
let appID = String(notification.rawValue).replacingOccurrences(of: baseName + ".", with: "")
|
||||
self.runningApplications.insert(appID)
|
||||
}
|
||||
|
||||
func scheduleFinishedRefreshingNotification(for result: Result<[String: Result<InstalledApp, Error>], Error>, delay: TimeInterval = 5)
|
||||
{
|
||||
func scheduleFinishedRefreshingNotification()
|
||||
{
|
||||
self.cancelFinishedRefreshingNotification()
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
var shouldPresentAlert = true
|
||||
|
||||
do
|
||||
{
|
||||
let results = try result.get()
|
||||
shouldPresentAlert = !results.isEmpty
|
||||
|
||||
for (_, result) in results
|
||||
{
|
||||
guard case let .failure(error) = result else { continue }
|
||||
throw error
|
||||
}
|
||||
|
||||
content.title = NSLocalizedString("Refreshed Apps", comment: "")
|
||||
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
|
||||
}
|
||||
catch ConnectionError.serverNotFound
|
||||
{
|
||||
shouldPresentAlert = false
|
||||
}
|
||||
catch RefreshError.noInstalledApps
|
||||
{
|
||||
shouldPresentAlert = false
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to refresh apps in background.", error)
|
||||
|
||||
content.title = NSLocalizedString("Failed to Refresh Apps", comment: "")
|
||||
content.body = error.localizedDescription
|
||||
|
||||
shouldPresentAlert = true
|
||||
}
|
||||
|
||||
if shouldPresentAlert
|
||||
{
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
|
||||
|
||||
let request = UNNotificationRequest(identifier: self.refreshIdentifier, content: content, trigger: trigger)
|
||||
UNUserNotificationCenter.current().add(request)
|
||||
|
||||
if delay > 0
|
||||
{
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests() { (requests) in
|
||||
// If app is still running at this point, we schedule another notification with same identifier.
|
||||
// This prevents the currently scheduled notification from displaying, and starts another countdown timer.
|
||||
// First though, make sure there _is_ still a pending request, otherwise it's been cancelled
|
||||
// and we should stop polling.
|
||||
guard requests.contains(where: { $0.identifier == self.refreshIdentifier }) else { return }
|
||||
|
||||
scheduleFinishedRefreshingNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.presentsFinishedNotification
|
||||
{
|
||||
scheduleFinishedRefreshingNotification()
|
||||
}
|
||||
|
||||
// Perform synchronously to ensure app doesn't quit before we've finishing saving to disk.
|
||||
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
context.performAndWait {
|
||||
_ = RefreshAttempt(identifier: self.refreshIdentifier, result: result, context: context)
|
||||
|
||||
do { try context.save() }
|
||||
catch { print("Failed to save refresh attempt.", error) }
|
||||
}
|
||||
}
|
||||
|
||||
func cancelFinishedRefreshingNotification()
|
||||
{
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [self.refreshIdentifier])
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltKit
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
extension BackupAppOperation
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import AltKit
|
||||
|
||||
import Roxas
|
||||
|
||||
@objc(DeactivateAppOperation)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
@objc(DownloadAppOperation)
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import AltKit
|
||||
|
||||
import Roxas
|
||||
|
||||
@objc(FetchAnisetteDataOperation)
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import AltKit
|
||||
|
||||
import Roxas
|
||||
|
||||
@objc(FetchAppIDsOperation)
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
@objc(FetchProvisioningProfilesOperation)
|
||||
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
@objc(FetchSourceOperation)
|
||||
@@ -49,7 +50,7 @@ class FetchSourceOperation: ResultOperation<Source>
|
||||
{
|
||||
let (data, _) = try Result((data, response), error).get()
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
let decoder = AltStoreCore.JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
|
||||
let container = try decoder.singleValueContainer()
|
||||
let text = try container.decode(String.self)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AltKit
|
||||
|
||||
import Roxas
|
||||
|
||||
private let ReceivedServerConnectionResponse: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import Roxas
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import CoreData
|
||||
import Network
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
class OperationContext
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
import AltKit
|
||||
|
||||
import Roxas
|
||||
|
||||
@objc(RefreshAppOperation)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
class RefreshGroup: NSObject
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltKit
|
||||
|
||||
@objc(RemoveAppBackupOperation)
|
||||
class RemoveAppBackupOperation: ResultOperation<Void>
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltKit
|
||||
import AltStoreCore
|
||||
|
||||
@objc(RemoveAppOperation)
|
||||
class RemoveAppOperation: ResultOperation<InstalledApp>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
import AltStoreCore
|
||||
import AltSign
|
||||
|
||||
@objc(ResignAppOperation)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
import AltStoreCore
|
||||
|
||||
@objc(SendAppOperation)
|
||||
class SendAppOperation: ResultOperation<ServerConnection>
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
import Foundation
|
||||
|
||||
import AltSign
|
||||
import AltKit
|
||||
|
||||
import Roxas
|
||||
|
||||
enum VerificationError: ALTLocalizedError
|
||||
|
||||
134
AltStore/SceneDelegate.swift
Normal file
134
AltStore/SceneDelegate.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// SceneDelegate.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/6/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AltStoreCore
|
||||
|
||||
@available(iOS 13, *)
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate
|
||||
{
|
||||
var window: UIWindow?
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
|
||||
{
|
||||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||
guard let _ = (scene as? UIWindowScene) else { return }
|
||||
|
||||
if let context = connectionOptions.urlContexts.first
|
||||
{
|
||||
self.open(context)
|
||||
}
|
||||
}
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene)
|
||||
{
|
||||
// Called as the scene transitions from the background to the foreground.
|
||||
// Use this method to undo the changes made on entering the background.
|
||||
|
||||
// applicationWillEnterForeground is _not_ called when launching app,
|
||||
// whereas sceneWillEnterForeground _is_ called when launching.
|
||||
// As a result, DatabaseManager might not be started yet, so just return if it isn't
|
||||
// (since all these methods are called separately during app startup).
|
||||
guard DatabaseManager.shared.isStarted else { return }
|
||||
|
||||
AppManager.shared.update()
|
||||
ServerManager.shared.startDiscovering()
|
||||
|
||||
PatreonAPI.shared.refreshPatreonAccount()
|
||||
}
|
||||
|
||||
func sceneDidEnterBackground(_ scene: UIScene)
|
||||
{
|
||||
// Called as the scene transitions from the foreground to the background.
|
||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||
// to restore the scene back to its current state.
|
||||
|
||||
guard UIApplication.shared.applicationState == .background else { return }
|
||||
|
||||
ServerManager.shared.stopDiscovering()
|
||||
}
|
||||
|
||||
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>)
|
||||
{
|
||||
guard let context = URLContexts.first else { return }
|
||||
self.open(context)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
private extension SceneDelegate
|
||||
{
|
||||
func open(_ context: UIOpenURLContext)
|
||||
{
|
||||
if context.url.isFileURL
|
||||
{
|
||||
guard context.url.pathExtension.lowercased() == "ipa" else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: context.url])
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
guard let components = URLComponents(url: context.url, resolvingAgainstBaseURL: false) else { return }
|
||||
guard let host = components.host?.lowercased() else { return }
|
||||
|
||||
switch host
|
||||
{
|
||||
case "patreon":
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||
}
|
||||
|
||||
case "appbackupresponse":
|
||||
let result: Result<Void, Error>
|
||||
|
||||
switch context.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 }
|
||||
|
||||
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
|
||||
result = .failure(error)
|
||||
|
||||
default: return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
|
||||
enum ConnectionError: LocalizedError
|
||||
{
|
||||
case serverNotFound
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
import AltStoreCore
|
||||
|
||||
class ServerConnection
|
||||
{
|
||||
@@ -95,7 +95,7 @@ class ServerConnection
|
||||
{
|
||||
let data = try self.process(data: data, error: error)
|
||||
|
||||
let response = try JSONDecoder().decode(ServerResponse.self, from: data)
|
||||
let response = try AltStoreCore.JSONDecoder().decode(ServerResponse.self, from: data)
|
||||
completionHandler(.success(response))
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
import AltKit
|
||||
import AltStoreCore
|
||||
|
||||
class ServerManager: NSObject
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ import UIKit
|
||||
import SafariServices
|
||||
import AuthenticationServices
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
extension PatreonViewController
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
@objc(RefreshAttemptTableViewCell)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="5Rz-4h-jJ8">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17124"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@@ -20,7 +20,7 @@
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<label key="tableFooterView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="AltStore 1.0" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="bUR-rp-Nw2">
|
||||
<rect key="frame" x="0.0" y="996.5" width="375" height="25"/>
|
||||
<rect key="frame" x="0.0" y="1047.5" width="375" height="25"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -74,7 +74,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Riley Testut" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" id="CnN-M1-AYK">
|
||||
<rect key="frame" x="251.5" y="16" width="93.5" height="20.5"/>
|
||||
<rect key="frame" x="252.5" y="16" width="92.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -106,7 +106,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="riley@altstore.io" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" id="0uP-Cd-tNX">
|
||||
<rect key="frame" x="215" y="16" width="130" height="20.5"/>
|
||||
<rect key="frame" x="215.5" y="16" width="129.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -138,7 +138,7 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Developer" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" id="434-MW-Den">
|
||||
<rect key="frame" x="263.5" y="16" width="81.5" height="20.5"/>
|
||||
<rect key="frame" x="264" y="16" width="81" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@@ -167,7 +167,7 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Join the beta" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Il-5a-5Zp">
|
||||
<rect key="frame" x="30" y="15" width="106.5" height="21"/>
|
||||
<rect key="frame" x="30" y="15" width="106" height="21"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -207,7 +207,7 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Background Refresh" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="EbG-HB-IOn">
|
||||
<rect key="frame" x="30" y="15" width="167" height="21"/>
|
||||
<rect key="frame" x="30" y="15" width="166" height="21"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -230,17 +230,45 @@
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="0"/>
|
||||
<integer key="value" value="1"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="amC-sE-8O0" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="461.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="amC-sE-8O0" id="GEO-2e-E4k">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Add to Siri…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6K-fI-CVr">
|
||||
<rect key="frame" x="30" y="15" width="101" height="21"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="c6K-fI-CVr" firstAttribute="centerY" secondItem="GEO-2e-E4k" secondAttribute="centerY" id="IGB-ox-RAM"/>
|
||||
<constraint firstItem="c6K-fI-CVr" firstAttribute="leading" secondItem="GEO-2e-E4k" secondAttribute="leadingMargin" id="xoI-eB-1TH"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="style">
|
||||
<integer key="value" value="3"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="" id="9ht-ML-85l">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="wYS-qW-u8S" rowHeight="51" style="IBUITableViewCellStyleDefault" id="ndZ-OC-MWv" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="501.5" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="552.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ndZ-OC-MWv" id="Tuq-2M-9df">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -270,14 +298,14 @@
|
||||
<tableViewSection headerTitle="" id="eHy-qI-w5w">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="30h-59-88f" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="592.5" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="643.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="30h-59-88f" id="7qD-DW-Jls">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="How it works" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2CC-iw-3bd">
|
||||
<rect key="frame" x="30" y="15" width="106" height="21"/>
|
||||
<rect key="frame" x="30" y="15" width="105" height="21"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -310,7 +338,7 @@
|
||||
<tableViewSection headerTitle="" id="J90-vn-u2O">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="i4T-2q-jF3" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="683.5" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="734.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="i4T-2q-jF3" id="VTQ-H4-aCM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -323,16 +351,16 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk">
|
||||
<rect key="frame" x="219.5" y="15.5" width="125.5" height="20.5"/>
|
||||
<rect key="frame" x="220.5" y="15.5" width="124.5" height="20.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Riley Testut" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JAA-iZ-VGb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="93.5" height="20.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="92.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb">
|
||||
<rect key="frame" x="107.5" y="0.0" width="18" height="20.5"/>
|
||||
<rect key="frame" x="106.5" y="0.0" width="18" height="20.5"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
@@ -354,7 +382,7 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="0MT-ht-Sit" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="734.5" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="785.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0MT-ht-Sit" id="OZp-WM-5H7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -367,16 +395,16 @@
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
|
||||
<rect key="frame" x="191.5" y="15.5" width="153.5" height="20.5"/>
|
||||
<rect key="frame" x="192.5" y="15.5" width="152.5" height="20.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Caroline Moore" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T">
|
||||
<rect key="frame" x="0.0" y="0.0" width="121.5" height="20.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="120.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
|
||||
<rect key="frame" x="135.5" y="0.0" width="18" height="20.5"/>
|
||||
<rect key="frame" x="134.5" y="0.0" width="18" height="20.5"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</stackView>
|
||||
@@ -398,7 +426,7 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="O5R-Al-lGj" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="785.5" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="836.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="O5R-Al-lGj" id="CrG-Mr-xQq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -438,7 +466,7 @@
|
||||
<tableViewSection headerTitle="" id="OMa-EK-hRI">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="876.5" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="927.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FMZ-as-Ljo" id="JzL-Of-A3T">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
@@ -471,14 +499,14 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="927.5" width="375" height="51"/>
|
||||
<rect key="frame" x="0.0" y="978.5" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qca-pU-sJh" id="QtU-8J-VQN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="View Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sni-07-q0M">
|
||||
<rect key="frame" x="30" y="15" width="189" height="21"/>
|
||||
<rect key="frame" x="30" y="15" width="187.5" height="21"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@@ -581,9 +609,9 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Success" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SWb-Of-t97">
|
||||
<rect key="frame" x="0.0" y="0.0" width="67.5" height="20.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="67" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<color key="textColor" systemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Date" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4MX-Qv-H8V">
|
||||
@@ -721,6 +749,7 @@ Settings by i cons from the Noun Project</string>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="o3f-Lj-IHF"/>
|
||||
<color key="backgroundColor" name="SettingsBackground"/>
|
||||
<constraints>
|
||||
<constraint firstItem="oQQ-pR-oKc" firstAttribute="top" secondItem="o3f-Lj-IHF" secondAttribute="top" id="3gx-wh-Lol"/>
|
||||
@@ -728,7 +757,6 @@ Settings by i cons from the Noun Project</string>
|
||||
<constraint firstItem="oQQ-pR-oKc" firstAttribute="leading" secondItem="o3f-Lj-IHF" secondAttribute="leading" id="PIZ-YA-eVd"/>
|
||||
<constraint firstItem="oQQ-pR-oKc" firstAttribute="trailing" secondItem="o3f-Lj-IHF" secondAttribute="trailing" id="oUR-b9-ajN"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="o3f-Lj-IHF"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" title="Software Licenses" largeTitleDisplayMode="never" id="JcT-wX-zay"/>
|
||||
<connections>
|
||||
@@ -807,5 +835,8 @@ Settings by i cons from the Noun Project</string>
|
||||
<namedColor name="SettingsBackground">
|
||||
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<systemColor name="darkTextColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
import UIKit
|
||||
import SafariServices
|
||||
import MessageUI
|
||||
import Intents
|
||||
import IntentsUI
|
||||
|
||||
import AltStoreCore
|
||||
|
||||
extension SettingsViewController
|
||||
{
|
||||
@@ -17,13 +21,26 @@ extension SettingsViewController
|
||||
case signIn
|
||||
case account
|
||||
case patreon
|
||||
case backgroundRefresh
|
||||
case appRefresh
|
||||
case jailbreak
|
||||
case instructions
|
||||
case credits
|
||||
case debug
|
||||
}
|
||||
|
||||
fileprivate enum AppRefreshRow: Int, CaseIterable
|
||||
{
|
||||
case backgroundRefresh
|
||||
|
||||
@available(iOS 14, *)
|
||||
case addToSiri
|
||||
|
||||
static var allCases: [AppRefreshRow] {
|
||||
guard #available(iOS 14, *) else { return [.backgroundRefresh] }
|
||||
return [.backgroundRefresh, .addToSiri]
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum CreditsRow: Int, CaseIterable
|
||||
{
|
||||
case developer
|
||||
@@ -165,8 +182,15 @@ private extension SettingsViewController
|
||||
settingsHeaderFooterView.button.addTarget(self, action: #selector(SettingsViewController.signOut(_:)), for: .primaryActionTriggered)
|
||||
settingsHeaderFooterView.button.isHidden = false
|
||||
|
||||
case .backgroundRefresh:
|
||||
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Automatically refresh apps in the background when connected to the same WiFi as AltServer.", comment: "")
|
||||
case .appRefresh:
|
||||
if isHeader
|
||||
{
|
||||
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("REFRESHING APPS", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Enable Background Refresh to automatically refresh apps in the background when connected to the same WiFi as AltServer.", comment: "")
|
||||
}
|
||||
|
||||
case .jailbreak:
|
||||
if isHeader
|
||||
@@ -254,6 +278,17 @@ private extension SettingsViewController
|
||||
UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
@IBAction func addRefreshAppsShortcut()
|
||||
{
|
||||
guard let shortcut = INShortcut(intent: INInteraction.refreshAllApps().intent) else { return }
|
||||
|
||||
let viewController = INUIAddVoiceShortcutViewController(shortcut: shortcut)
|
||||
viewController.delegate = self
|
||||
viewController.modalPresentationStyle = .formSheet
|
||||
self.present(viewController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@IBAction func handleDebugModeGesture(_ gestureRecognizer: UISwipeGestureRecognizer)
|
||||
{
|
||||
self.debugGestureCounter += 1
|
||||
@@ -331,11 +366,28 @@ extension SettingsViewController
|
||||
{
|
||||
case .signIn: return (self.activeTeam == nil) ? 1 : 0
|
||||
case .account: return (self.activeTeam == nil) ? 0 : 3
|
||||
case .appRefresh: return AppRefreshRow.allCases.count
|
||||
case .jailbreak: return UIDevice.current.isJailbroken ? 1 : 0
|
||||
default: return super.tableView(tableView, numberOfRowsInSection: section.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
|
||||
{
|
||||
let cell = super.tableView(tableView, cellForRowAt: indexPath)
|
||||
|
||||
if #available(iOS 14, *) {}
|
||||
else if let cell = cell as? InsetGroupTableViewCell,
|
||||
indexPath.section == Section.appRefresh.rawValue,
|
||||
indexPath.row == AppRefreshRow.backgroundRefresh.rawValue
|
||||
{
|
||||
// Only one row is visible pre-iOS 14.
|
||||
cell.style = .single
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
|
||||
{
|
||||
let section = Section.allCases[section]
|
||||
@@ -345,12 +397,12 @@ extension SettingsViewController
|
||||
case .account where self.activeTeam == nil: return nil
|
||||
case .jailbreak where !UIDevice.current.isJailbroken: return nil
|
||||
|
||||
case .signIn, .account, .patreon, .jailbreak, .credits, .debug:
|
||||
case .signIn, .account, .patreon, .appRefresh, .jailbreak, .credits, .debug:
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
|
||||
self.prepare(headerView, for: section, isHeader: true)
|
||||
return headerView
|
||||
|
||||
case .backgroundRefresh, .instructions: return nil
|
||||
case .instructions: return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +414,7 @@ extension SettingsViewController
|
||||
case .signIn where self.activeTeam != nil: return nil
|
||||
case .jailbreak where !UIDevice.current.isJailbroken: return nil
|
||||
|
||||
case .signIn, .patreon, .backgroundRefresh, .jailbreak:
|
||||
case .signIn, .patreon, .appRefresh, .jailbreak:
|
||||
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
|
||||
self.prepare(footerView, for: section, isHeader: false)
|
||||
return footerView
|
||||
@@ -380,11 +432,11 @@ extension SettingsViewController
|
||||
case .account where self.activeTeam == nil: return 1.0
|
||||
case .jailbreak where !UIDevice.current.isJailbroken: return 1.0
|
||||
|
||||
case .signIn, .account, .patreon, .jailbreak, .credits, .debug:
|
||||
case .signIn, .account, .patreon, .appRefresh, .jailbreak, .credits, .debug:
|
||||
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
|
||||
return height
|
||||
|
||||
case .backgroundRefresh, .instructions: return 0.0
|
||||
case .instructions: return 0.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,7 +449,7 @@ extension SettingsViewController
|
||||
case .account where self.activeTeam == nil: return 1.0
|
||||
case .jailbreak where !UIDevice.current.isJailbroken: return 1.0
|
||||
|
||||
case .signIn, .patreon, .backgroundRefresh, .jailbreak:
|
||||
case .signIn, .patreon, .appRefresh, .jailbreak:
|
||||
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
|
||||
return height
|
||||
|
||||
@@ -415,6 +467,16 @@ extension SettingsViewController
|
||||
{
|
||||
case .signIn: self.signIn()
|
||||
case .instructions: break
|
||||
case .appRefresh:
|
||||
let row = AppRefreshRow.allCases[indexPath.row]
|
||||
switch row
|
||||
{
|
||||
case .backgroundRefresh: break
|
||||
case .addToSiri:
|
||||
guard #available(iOS 14, *) else { return }
|
||||
self.addRefreshAppsShortcut()
|
||||
}
|
||||
|
||||
case .jailbreak:
|
||||
let fileURL = Bundle.main.url(forResource: "AltDaemon", withExtension: "deb")!
|
||||
|
||||
@@ -489,3 +551,31 @@ extension SettingsViewController: UIGestureRecognizerDelegate
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController: INUIAddVoiceShortcutViewControllerDelegate
|
||||
{
|
||||
func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?)
|
||||
{
|
||||
if let indexPath = self.tableView.indexPathForSelectedRow
|
||||
{
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
controller.dismiss(animated: true, completion: nil)
|
||||
|
||||
guard let error = error else { return }
|
||||
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
|
||||
func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController)
|
||||
{
|
||||
if let indexPath = self.tableView.indexPathForSelectedRow
|
||||
{
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
controller.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import UIKit
|
||||
import CoreData
|
||||
|
||||
import AltStoreCore
|
||||
import Roxas
|
||||
|
||||
class SourcesViewController: UICollectionViewController
|
||||
|
||||
27
AltStoreCore/AltStoreCore.h
Normal file
27
AltStoreCore/AltStoreCore.h
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// AltStoreCore.h
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 9/3/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for AltStoreCore.
|
||||
FOUNDATION_EXPORT double AltStoreCoreVersionNumber;
|
||||
|
||||
//! Project version string for AltStoreCore.
|
||||
FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <AltStoreCore/PublicHeader.h>
|
||||
|
||||
#import <AltStoreCore/ALTAppPermission.h>
|
||||
#import <AltStoreCore/ALTSourceUserInfoKey.h>
|
||||
#import <AltStoreCore/ALTPatreonBenefitType.h>
|
||||
|
||||
// Shared
|
||||
#import <AltStoreCore/ALTConstants.h>
|
||||
#import <AltStoreCore/ALTConnection.h>
|
||||
#import <AltStoreCore/NSError+ALTServerError.h>
|
||||
#import <AltStoreCore/CFNotificationName+AltStore.h>
|
||||
@@ -12,11 +12,11 @@ import KeychainAccess
|
||||
import AltSign
|
||||
|
||||
@propertyWrapper
|
||||
struct KeychainItem<Value>
|
||||
public struct KeychainItem<Value>
|
||||
{
|
||||
let key: String
|
||||
public let key: String
|
||||
|
||||
var wrappedValue: Value? {
|
||||
public var wrappedValue: Value? {
|
||||
get {
|
||||
switch Value.self
|
||||
{
|
||||
@@ -35,50 +35,50 @@ struct KeychainItem<Value>
|
||||
}
|
||||
}
|
||||
|
||||
init(key: String)
|
||||
public init(key: String)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
}
|
||||
|
||||
class Keychain
|
||||
public class Keychain
|
||||
{
|
||||
static let shared = Keychain()
|
||||
public static let shared = Keychain()
|
||||
|
||||
fileprivate let keychain = KeychainAccess.Keychain(service: "com.rileytestut.AltStore").accessibility(.afterFirstUnlock).synchronizable(true)
|
||||
|
||||
@KeychainItem(key: "appleIDEmailAddress")
|
||||
var appleIDEmailAddress: String?
|
||||
public var appleIDEmailAddress: String?
|
||||
|
||||
@KeychainItem(key: "appleIDPassword")
|
||||
var appleIDPassword: String?
|
||||
public var appleIDPassword: String?
|
||||
|
||||
@KeychainItem(key: "signingCertificatePrivateKey")
|
||||
var signingCertificatePrivateKey: Data?
|
||||
public var signingCertificatePrivateKey: Data?
|
||||
|
||||
@KeychainItem(key: "signingCertificateSerialNumber")
|
||||
var signingCertificateSerialNumber: String?
|
||||
public var signingCertificateSerialNumber: String?
|
||||
|
||||
@KeychainItem(key: "signingCertificate")
|
||||
var signingCertificate: Data?
|
||||
public var signingCertificate: Data?
|
||||
|
||||
@KeychainItem(key: "signingCertificatePassword")
|
||||
var signingCertificatePassword: String?
|
||||
public var signingCertificatePassword: String?
|
||||
|
||||
@KeychainItem(key: "patreonAccessToken")
|
||||
var patreonAccessToken: String?
|
||||
public var patreonAccessToken: String?
|
||||
|
||||
@KeychainItem(key: "patreonRefreshToken")
|
||||
var patreonRefreshToken: String?
|
||||
public var patreonRefreshToken: String?
|
||||
|
||||
@KeychainItem(key: "patreonCreatorAccessToken")
|
||||
var patreonCreatorAccessToken: String?
|
||||
public var patreonCreatorAccessToken: String?
|
||||
|
||||
private init()
|
||||
{
|
||||
}
|
||||
|
||||
func reset()
|
||||
public func reset()
|
||||
{
|
||||
self.appleIDEmailAddress = nil
|
||||
self.appleIDPassword = nil
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
extension CodingUserInfoKey
|
||||
public extension CodingUserInfoKey
|
||||
{
|
||||
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
|
||||
static let sourceURL = CodingUserInfoKey(rawValue: "sourceURL")!
|
||||
@@ -18,29 +18,29 @@ extension CodingUserInfoKey
|
||||
public final class JSONDecoder: Foundation.JSONDecoder
|
||||
{
|
||||
@DecoderItem(key: .managedObjectContext)
|
||||
var managedObjectContext: NSManagedObjectContext?
|
||||
public var managedObjectContext: NSManagedObjectContext?
|
||||
|
||||
@DecoderItem(key: .sourceURL)
|
||||
var sourceURL: URL?
|
||||
public var sourceURL: URL?
|
||||
}
|
||||
|
||||
extension Decoder
|
||||
public extension Decoder
|
||||
{
|
||||
var managedObjectContext: NSManagedObjectContext? { self.userInfo[.managedObjectContext] as? NSManagedObjectContext }
|
||||
var sourceURL: URL? { self.userInfo[.sourceURL] as? URL }
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
struct DecoderItem<Value>
|
||||
public struct DecoderItem<Value>
|
||||
{
|
||||
let key: CodingUserInfoKey
|
||||
public let key: CodingUserInfoKey
|
||||
|
||||
var wrappedValue: Value? {
|
||||
public var wrappedValue: Value? {
|
||||
get { fatalError("only works on instance properties of classes") }
|
||||
set { fatalError("only works on instance properties of classes") }
|
||||
}
|
||||
|
||||
init(key: CodingUserInfoKey)
|
||||
public init(key: CodingUserInfoKey)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
17
AltStoreCore/Extensions/UIApplication+AppExtension.swift
Normal file
17
AltStoreCore/Extensions/UIApplication+AppExtension.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// UIApplication+AppExtension.swift
|
||||
// DeltaCore
|
||||
//
|
||||
// Created by Riley Testut on 6/14/18.
|
||||
// Copyright © 2018 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
public extension UIApplication
|
||||
{
|
||||
// Cannot normally use UIApplication.shared from extensions, so we get around this by calling value(forKey:).
|
||||
class var alt_shared: UIApplication? {
|
||||
return UIApplication.value(forKey: "sharedApplication") as? UIApplication
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIColor
|
||||
public extension UIColor
|
||||
{
|
||||
// Borrowed from https://stackoverflow.com/a/26341062
|
||||
var hexString: String {
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
|
||||
import Roxas
|
||||
|
||||
extension UserDefaults
|
||||
public extension UserDefaults
|
||||
{
|
||||
@NSManaged var firstLaunch: Date?
|
||||
|
||||
22
AltStoreCore/Info.plist
Normal file
22
AltStoreCore/Info.plist
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -12,9 +12,9 @@ import CoreData
|
||||
import AltSign
|
||||
|
||||
@objc(Account)
|
||||
class Account: NSManagedObject, Fetchable
|
||||
public class Account: NSManagedObject, Fetchable
|
||||
{
|
||||
var localizedName: String {
|
||||
public var localizedName: String {
|
||||
var components = PersonNameComponents()
|
||||
components.givenName = self.firstName
|
||||
components.familyName = self.lastName
|
||||
@@ -24,30 +24,30 @@ class Account: NSManagedObject, Fetchable
|
||||
}
|
||||
|
||||
/* Properties */
|
||||
@NSManaged var appleID: String
|
||||
@NSManaged var identifier: String
|
||||
@NSManaged public var appleID: String
|
||||
@NSManaged public var identifier: String
|
||||
|
||||
@NSManaged var firstName: String
|
||||
@NSManaged var lastName: String
|
||||
@NSManaged public var firstName: String
|
||||
@NSManaged public var lastName: String
|
||||
|
||||
@NSManaged var isActiveAccount: Bool
|
||||
@NSManaged public var isActiveAccount: Bool
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var teams: Set<Team>
|
||||
@NSManaged public var teams: Set<Team>
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(_ account: ALTAccount, context: NSManagedObjectContext)
|
||||
public init(_ account: ALTAccount, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: Account.entity(), insertInto: context)
|
||||
|
||||
self.update(account: account)
|
||||
}
|
||||
|
||||
func update(account: ALTAccount)
|
||||
public func update(account: ALTAccount)
|
||||
{
|
||||
self.appleID = account.appleID
|
||||
self.identifier = account.identifier
|
||||
@@ -57,7 +57,7 @@ class Account: NSManagedObject, Fetchable
|
||||
}
|
||||
}
|
||||
|
||||
extension Account
|
||||
public extension Account
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Account>
|
||||
{
|
||||
@@ -3,6 +3,6 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>AltStore 7.xcdatamodel</string>
|
||||
<string>AltStore 8.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,175 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17189" systemVersion="20A5354i" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String"/>
|
||||
<attribute name="firstName" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveAccount" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastName" attributeType="String"/>
|
||||
<relationship name="teams" toMany="YES" deletionRule="Cascade" destinationEntity="Team" inverseName="account" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppID" representedClassName="AppID" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="features" attributeType="Transformable"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="appIDs" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
|
||||
<attribute name="type" attributeType="String"/>
|
||||
<attribute name="usageDescription" attributeType="String"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="permissions" inverseEntity="StoreApp"/>
|
||||
</entity>
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="isRefreshing" transient="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="appExtensions" toMany="YES" deletionRule="Cascade" destinationEntity="InstalledExtension" inverseName="parentApp" inverseEntity="InstalledExtension"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="installedApp" inverseEntity="StoreApp"/>
|
||||
<relationship name="team" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Team" inverseName="installedApps" inverseEntity="Team"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="InstalledExtension" representedClassName="InstalledExtension" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<relationship name="parentApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="appExtensions" inverseEntity="InstalledApp"/>
|
||||
</entity>
|
||||
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||
<attribute name="appID" optional="YES" attributeType="String"/>
|
||||
<attribute name="caption" attributeType="String"/>
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="externalURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||
<attribute name="firstName" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isPatron" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="errorDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="error" optional="YES" attributeType="Transformable" valueTransformerName="ALTSecureValueTransformer"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="StoreApp" representedClassName="StoreApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String"/>
|
||||
<attribute name="developerName" attributeType="String"/>
|
||||
<attribute name="downloadURL" attributeType="URI"/>
|
||||
<attribute name="iconURL" attributeType="URI"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="versionDescription" optional="YES" attributeType="String"/>
|
||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem"/>
|
||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="teams" inverseEntity="Account"/>
|
||||
<relationship name="appIDs" toMany="YES" deletionRule="Cascade" destinationEntity="AppID" inverseName="team" inverseEntity="AppID"/>
|
||||
<relationship name="installedApps" toMany="YES" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="team" inverseEntity="InstalledApp"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="224"/>
|
||||
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
|
||||
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="133"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -12,24 +12,24 @@ import CoreData
|
||||
import AltSign
|
||||
|
||||
@objc(AppID)
|
||||
class AppID: NSManagedObject, Fetchable
|
||||
public class AppID: NSManagedObject, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var name: String
|
||||
@NSManaged var identifier: String
|
||||
@NSManaged var bundleIdentifier: String
|
||||
@NSManaged var features: [ALTFeature: Any]
|
||||
@NSManaged var expirationDate: Date?
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var identifier: String
|
||||
@NSManaged public var bundleIdentifier: String
|
||||
@NSManaged public var features: [ALTFeature: Any]
|
||||
@NSManaged public var expirationDate: Date?
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged private(set) var team: Team?
|
||||
@NSManaged public private(set) var team: Team?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(_ appID: ALTAppID, team: Team, context: NSManagedObjectContext)
|
||||
public init(_ appID: ALTAppID, team: Team, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: AppID.entity(), insertInto: context)
|
||||
|
||||
@@ -43,7 +43,7 @@ class AppID: NSManagedObject, Fetchable
|
||||
}
|
||||
}
|
||||
|
||||
extension AppID
|
||||
public extension AppID
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<AppID>
|
||||
{
|
||||
@@ -9,7 +9,7 @@
|
||||
import CoreData
|
||||
import UIKit
|
||||
|
||||
extension ALTAppPermissionType
|
||||
public extension ALTAppPermissionType
|
||||
{
|
||||
var localizedShortName: String? {
|
||||
switch self
|
||||
@@ -43,14 +43,14 @@ extension ALTAppPermissionType
|
||||
}
|
||||
|
||||
@objc(AppPermission)
|
||||
class AppPermission: NSManagedObject, Decodable, Fetchable
|
||||
public class AppPermission: NSManagedObject, Decodable, Fetchable
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var type: ALTAppPermissionType
|
||||
@NSManaged var usageDescription: String
|
||||
@NSManaged public var type: ALTAppPermissionType
|
||||
@NSManaged public var usageDescription: String
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged private(set) var app: StoreApp!
|
||||
@NSManaged public private(set) var app: StoreApp!
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
@@ -63,7 +63,7 @@ class AppPermission: NSManagedObject, Decodable, Fetchable
|
||||
case usageDescription
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws
|
||||
public required init(from decoder: Decoder) throws
|
||||
{
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
|
||||
@@ -89,7 +89,7 @@ class AppPermission: NSManagedObject, Decodable, Fetchable
|
||||
}
|
||||
}
|
||||
|
||||
extension AppPermission
|
||||
public extension AppPermission
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<AppPermission>
|
||||
{
|
||||
@@ -20,10 +20,11 @@ public class DatabaseManager
|
||||
public private(set) var isStarted = false
|
||||
|
||||
private var startCompletionHandlers = [(Error?) -> Void]()
|
||||
private let dispatchQueue = DispatchQueue(label: "io.altstore.DatabaseManager")
|
||||
|
||||
private init()
|
||||
{
|
||||
self.persistentContainer = RSTPersistentContainer(name: "AltStore")
|
||||
self.persistentContainer = RSTPersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
|
||||
self.persistentContainer.preferredMergePolicy = MergePolicy()
|
||||
}
|
||||
}
|
||||
@@ -32,30 +33,34 @@ public extension DatabaseManager
|
||||
{
|
||||
func start(completionHandler: @escaping (Error?) -> Void)
|
||||
{
|
||||
self.startCompletionHandlers.append(completionHandler)
|
||||
|
||||
guard self.startCompletionHandlers.count == 1 else { return }
|
||||
|
||||
func finish(_ error: Error?)
|
||||
{
|
||||
self.startCompletionHandlers.forEach { $0(error) }
|
||||
self.startCompletionHandlers.removeAll()
|
||||
self.dispatchQueue.async {
|
||||
if error == nil
|
||||
{
|
||||
self.isStarted = true
|
||||
}
|
||||
|
||||
self.startCompletionHandlers.forEach { $0(error) }
|
||||
self.startCompletionHandlers.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
guard !self.isStarted else { return finish(nil) }
|
||||
self.dispatchQueue.async {
|
||||
self.startCompletionHandlers.append(completionHandler)
|
||||
guard self.startCompletionHandlers.count == 1 else { return }
|
||||
|
||||
self.persistentContainer.loadPersistentStores { (description, error) in
|
||||
guard error == nil else { return finish(error!) }
|
||||
guard !self.isStarted else { return finish(nil) }
|
||||
|
||||
self.prepareDatabase() { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
finish(error)
|
||||
self.persistentContainer.loadPersistentStores { (description, error) in
|
||||
guard error == nil else { return finish(error!) }
|
||||
|
||||
case .success:
|
||||
self.isStarted = true
|
||||
finish(nil)
|
||||
self.prepareDatabase() { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): finish(error)
|
||||
case .success: finish(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +103,7 @@ public extension DatabaseManager
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseManager
|
||||
public extension DatabaseManager
|
||||
{
|
||||
func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account?
|
||||
{
|
||||
@@ -12,9 +12,9 @@ import CoreData
|
||||
import AltSign
|
||||
|
||||
// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1.
|
||||
let ALTActiveAppsLimit = 3
|
||||
public let ALTActiveAppsLimit = 3
|
||||
|
||||
protocol InstalledAppProtocol: Fetchable
|
||||
public protocol InstalledAppProtocol: Fetchable
|
||||
{
|
||||
var name: String { get }
|
||||
var bundleIdentifier: String { get }
|
||||
@@ -27,36 +27,39 @@ protocol InstalledAppProtocol: Fetchable
|
||||
}
|
||||
|
||||
@objc(InstalledApp)
|
||||
class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
public class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var name: String
|
||||
@NSManaged var bundleIdentifier: String
|
||||
@NSManaged var resignedBundleIdentifier: String
|
||||
@NSManaged var version: String
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var bundleIdentifier: String
|
||||
@NSManaged public var resignedBundleIdentifier: String
|
||||
@NSManaged public var version: String
|
||||
|
||||
@NSManaged var refreshedDate: Date
|
||||
@NSManaged var expirationDate: Date
|
||||
@NSManaged var installedDate: Date
|
||||
@NSManaged public var refreshedDate: Date
|
||||
@NSManaged public var expirationDate: Date
|
||||
@NSManaged public var installedDate: Date
|
||||
|
||||
@NSManaged var isActive: Bool
|
||||
@NSManaged public var isActive: Bool
|
||||
|
||||
@NSManaged var certificateSerialNumber: String?
|
||||
@NSManaged public var certificateSerialNumber: String?
|
||||
|
||||
/* Transient */
|
||||
@NSManaged public var isRefreshing: Bool
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var storeApp: StoreApp?
|
||||
@NSManaged var team: Team?
|
||||
@NSManaged var appExtensions: Set<InstalledExtension>
|
||||
@NSManaged public var storeApp: StoreApp?
|
||||
@NSManaged public var team: Team?
|
||||
@NSManaged public var appExtensions: Set<InstalledExtension>
|
||||
|
||||
var isSideloaded: Bool {
|
||||
public var isSideloaded: Bool {
|
||||
return self.storeApp == nil
|
||||
}
|
||||
|
||||
var appIDCount: Int {
|
||||
public var appIDCount: Int {
|
||||
return 1 + self.appExtensions.count
|
||||
}
|
||||
|
||||
var requiredActiveSlots: Int {
|
||||
public var requiredActiveSlots: Int {
|
||||
let requiredActiveSlots = UserDefaults.standard.activeAppLimitIncludesExtensions ? self.appIDCount : 1
|
||||
return requiredActiveSlots
|
||||
}
|
||||
@@ -66,7 +69,7 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext)
|
||||
public init(resignedApp: ALTApplication, originalBundleIdentifier: String, certificateSerialNumber: String?, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: InstalledApp.entity(), insertInto: context)
|
||||
|
||||
@@ -80,7 +83,7 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber)
|
||||
}
|
||||
|
||||
func update(resignedApp: ALTApplication, certificateSerialNumber: String?)
|
||||
public func update(resignedApp: ALTApplication, certificateSerialNumber: String?)
|
||||
{
|
||||
self.name = resignedApp.name
|
||||
|
||||
@@ -95,14 +98,14 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
}
|
||||
}
|
||||
|
||||
func update(provisioningProfile: ALTProvisioningProfile)
|
||||
public func update(provisioningProfile: ALTProvisioningProfile)
|
||||
{
|
||||
self.refreshedDate = provisioningProfile.creationDate
|
||||
self.expirationDate = provisioningProfile.expirationDate
|
||||
}
|
||||
}
|
||||
|
||||
extension InstalledApp
|
||||
public extension InstalledApp
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledApp>
|
||||
{
|
||||
@@ -199,7 +202,7 @@ extension InstalledApp
|
||||
}
|
||||
}
|
||||
|
||||
extension InstalledApp
|
||||
public extension InstalledApp
|
||||
{
|
||||
var openAppURL: URL {
|
||||
let openAppURL = URL(string: "altstore-" + self.bundleIdentifier + "://")!
|
||||
@@ -213,7 +216,7 @@ extension InstalledApp
|
||||
}
|
||||
}
|
||||
|
||||
extension InstalledApp
|
||||
public extension InstalledApp
|
||||
{
|
||||
class var appsDirectoryURL: URL {
|
||||
let appsDirectoryURL = FileManager.default.applicationSupportDirectory.appendingPathComponent("Apps")
|
||||
@@ -12,27 +12,27 @@ import CoreData
|
||||
import AltSign
|
||||
|
||||
@objc(InstalledExtension)
|
||||
class InstalledExtension: NSManagedObject, InstalledAppProtocol
|
||||
public class InstalledExtension: NSManagedObject, InstalledAppProtocol
|
||||
{
|
||||
/* Properties */
|
||||
@NSManaged var name: String
|
||||
@NSManaged var bundleIdentifier: String
|
||||
@NSManaged var resignedBundleIdentifier: String
|
||||
@NSManaged var version: String
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var bundleIdentifier: String
|
||||
@NSManaged public var resignedBundleIdentifier: String
|
||||
@NSManaged public var version: String
|
||||
|
||||
@NSManaged var refreshedDate: Date
|
||||
@NSManaged var expirationDate: Date
|
||||
@NSManaged var installedDate: Date
|
||||
@NSManaged public var refreshedDate: Date
|
||||
@NSManaged public var expirationDate: Date
|
||||
@NSManaged public var installedDate: Date
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var parentApp: InstalledApp?
|
||||
@NSManaged public var parentApp: InstalledApp?
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
}
|
||||
|
||||
init(resignedAppExtension: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext)
|
||||
public init(resignedAppExtension: ALTApplication, originalBundleIdentifier: String, context: NSManagedObjectContext)
|
||||
{
|
||||
super.init(entity: InstalledExtension.entity(), insertInto: context)
|
||||
|
||||
@@ -46,7 +46,7 @@ class InstalledExtension: NSManagedObject, InstalledAppProtocol
|
||||
self.update(resignedAppExtension: resignedAppExtension)
|
||||
}
|
||||
|
||||
func update(resignedAppExtension: ALTApplication)
|
||||
public func update(resignedAppExtension: ALTApplication)
|
||||
{
|
||||
self.name = resignedAppExtension.name
|
||||
|
||||
@@ -59,14 +59,14 @@ class InstalledExtension: NSManagedObject, InstalledAppProtocol
|
||||
}
|
||||
}
|
||||
|
||||
func update(provisioningProfile: ALTProvisioningProfile)
|
||||
public func update(provisioningProfile: ALTProvisioningProfile)
|
||||
{
|
||||
self.refreshedDate = provisioningProfile.creationDate
|
||||
self.expirationDate = provisioningProfile.expirationDate
|
||||
}
|
||||
}
|
||||
|
||||
extension InstalledExtension
|
||||
public extension InstalledExtension
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledExtension>
|
||||
{
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user