Merge branch 'module_refactoring' into develop

This commit is contained in:
Riley Testut
2020-09-09 10:41:17 -07:00
413 changed files with 3942 additions and 3976 deletions

View File

@@ -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

View File

@@ -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 }

View File

@@ -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)
{

View File

@@ -9,6 +9,6 @@
import Foundation
autoreleasepool {
ConnectionManager.shared.start()
DaemonConnectionManager.shared.start()
RunLoop.current.run()
}

View File

@@ -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"

View File

@@ -7,7 +7,6 @@
//
import Foundation
import AltKit
class AnisetteDataManager: NSObject
{

View File

@@ -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)

View File

@@ -6,7 +6,7 @@
// Copyright © 2020 Riley Testut. All rights reserved.
//
#import <AltSign/AltSign.h>
#import "AltSign.h"
NS_ASSUME_NONNULL_BEGIN

View File

@@ -7,7 +7,8 @@
//
#import "ALTNotificationConnection+Private.h"
#import "AltKit.h"
#import "NSError+ALTServerError.h"
void ALTDeviceReceivedNotification(const char *notification, void *user_data);

View File

@@ -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

View File

@@ -7,7 +7,9 @@
//
#import "ALTWiredConnection+Private.h"
#import "AltKit.h"
#import "ALTConnection.h"
#import "NSError+ALTServerError.h"
@implementation ALTWiredConnection

View File

@@ -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))
}

View File

@@ -7,7 +7,6 @@
//
import Foundation
import AltKit
class WiredConnectionHandler: ConnectionHandler
{

View File

@@ -9,8 +9,6 @@
import Foundation
import Network
import AltKit
extension WirelessConnectionHandler
{
public enum State

View File

@@ -7,7 +7,7 @@
//
#import <Foundation/Foundation.h>
#import <AltSign/AltSign.h>
#import "AltSign.h"
@class ALTWiredConnection;
@class ALTNotificationConnection;

View File

@@ -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

View File

@@ -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>

View File

@@ -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">

View File

@@ -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"

View File

@@ -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>

View File

@@ -8,6 +8,8 @@
import Foundation
import AltStoreCore
import AppCenter
import AppCenterAnalytics
import AppCenterCrashes

View File

@@ -8,6 +8,7 @@
import UIKit
import AltStoreCore
import Roxas
import Nuke

View File

@@ -8,6 +8,7 @@
import UIKit
import AltStoreCore
import Roxas
import Nuke

View File

@@ -8,6 +8,8 @@
import UIKit
import AltStoreCore
class PermissionPopoverViewController: UIViewController
{
var permission: AppPermission!

View File

@@ -8,6 +8,7 @@
import UIKit
import AltStoreCore
import Roxas
class AppIDsViewController: UICollectionViewController

View File

@@ -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])
}
}

View File

@@ -7,8 +7,9 @@
//
import UIKit
import AltSign
import AltStoreCore
import AltSign
import Roxas
class RefreshAltStoreViewController: UIViewController

View File

@@ -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)

View File

@@ -7,6 +7,8 @@
//
import UIKit
import AltStoreCore
import Roxas
class AppBannerView: RSTNibView

View File

@@ -8,6 +8,8 @@
import Roxas
import AltStoreCore
extension TimeInterval
{
static let shortToastViewDuration = 4.0

View File

@@ -8,7 +8,7 @@
import Foundation
import AltKit
import AltStoreCore
extension FileManager
{

View 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
}
}

View File

@@ -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>

View 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))
}
}
}
}
}

View 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>

View File

@@ -9,6 +9,8 @@
import UIKit
import Roxas
import AltStoreCore
class LaunchViewController: RSTLaunchViewController
{
private var didFinishLaunching = false

View File

@@ -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

View File

@@ -9,6 +9,8 @@
import Foundation
import CoreData
import AltStoreCore
extension AppManager
{
struct FetchSourcesError: LocalizedError, CustomNSError

View File

@@ -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)

View File

@@ -9,6 +9,7 @@
import UIKit
import SafariServices
import AltStoreCore
import Roxas
import Nuke

View File

@@ -10,7 +10,7 @@ import Foundation
import Roxas
import Network
import AltKit
import AltStoreCore
import AltSign
enum AuthenticationError: LocalizedError

View 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])
}
}

View File

@@ -8,7 +8,7 @@
import Foundation
import AltKit
import AltStoreCore
import AltSign
extension BackupAppOperation

View File

@@ -8,9 +8,8 @@
import Foundation
import AltStoreCore
import AltSign
import AltKit
import Roxas
@objc(DeactivateAppOperation)

View File

@@ -9,6 +9,7 @@
import Foundation
import Roxas
import AltStoreCore
import AltSign
@objc(DownloadAppOperation)

View File

@@ -8,9 +8,8 @@
import Foundation
import AltStoreCore
import AltSign
import AltKit
import Roxas
@objc(FetchAnisetteDataOperation)

View File

@@ -8,9 +8,8 @@
import Foundation
import AltStoreCore
import AltSign
import AltKit
import Roxas
@objc(FetchAppIDsOperation)

View File

@@ -7,9 +7,10 @@
//
import Foundation
import Roxas
import AltStoreCore
import AltSign
import Roxas
@objc(FetchProvisioningProfilesOperation)
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>

View File

@@ -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)

View File

@@ -7,7 +7,7 @@
//
import Foundation
import AltKit
import Roxas
private let ReceivedServerConnectionResponse: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =

View File

@@ -9,7 +9,7 @@
import Foundation
import Network
import AltKit
import AltStoreCore
import AltSign
import Roxas

View File

@@ -10,6 +10,7 @@ import Foundation
import CoreData
import Network
import AltStoreCore
import AltSign
class OperationContext

View File

@@ -8,9 +8,8 @@
import Foundation
import AltStoreCore
import AltSign
import AltKit
import Roxas
@objc(RefreshAppOperation)

View File

@@ -9,6 +9,7 @@
import Foundation
import CoreData
import AltStoreCore
import AltSign
class RefreshGroup: NSObject

View File

@@ -8,8 +8,6 @@
import Foundation
import AltKit
@objc(RemoveAppBackupOperation)
class RemoveAppBackupOperation: ResultOperation<Void>
{

View File

@@ -8,7 +8,7 @@
import Foundation
import AltKit
import AltStoreCore
@objc(RemoveAppOperation)
class RemoveAppOperation: ResultOperation<InstalledApp>

View File

@@ -9,6 +9,7 @@
import Foundation
import Roxas
import AltStoreCore
import AltSign
@objc(ResignAppOperation)

View File

@@ -9,7 +9,7 @@
import Foundation
import Network
import AltKit
import AltStoreCore
@objc(SendAppOperation)
class SendAppOperation: ResultOperation<ServerConnection>

View File

@@ -9,8 +9,6 @@
import Foundation
import AltSign
import AltKit
import Roxas
enum VerificationError: ALTLocalizedError

View 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
}
}
}
}

View File

@@ -8,8 +8,6 @@
import Network
import AltKit
enum ConnectionError: LocalizedError
{
case serverNotFound

View File

@@ -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

View File

@@ -9,7 +9,7 @@
import Foundation
import Network
import AltKit
import AltStoreCore
class ServerManager: NSObject
{

View File

@@ -10,6 +10,7 @@ import UIKit
import SafariServices
import AuthenticationServices
import AltStoreCore
import Roxas
extension PatreonViewController

View File

@@ -8,6 +8,7 @@
import UIKit
import AltStoreCore
import Roxas
@objc(RefreshAttemptTableViewCell)

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -9,6 +9,7 @@
import UIKit
import CoreData
import AltStoreCore
import Roxas
class SourcesViewController: UICollectionViewController

View 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>

View File

@@ -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

View File

@@ -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
}

View 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
}
}

View File

@@ -8,7 +8,7 @@
import UIKit
extension UIColor
public extension UIColor
{
// Borrowed from https://stackoverflow.com/a/26341062
var hexString: String {

View File

@@ -10,7 +10,7 @@ import Foundation
import Roxas
extension UserDefaults
public extension UserDefaults
{
@NSManaged var firstLaunch: Date?

22
AltStoreCore/Info.plist Normal file
View 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>

View File

@@ -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>
{

View File

@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>AltStore 7.xcdatamodel</string>
<string>AltStore 8.xcdatamodel</string>
</dict>
</plist>

View File

@@ -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>

View File

@@ -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>
{

View File

@@ -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>
{

View File

@@ -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?
{

View File

@@ -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")

View File

@@ -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