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> #import <Foundation/Foundation.h>
// Shared
#import "ALTConstants.h"
#import "ALTConnection.h"
#import "NSError+ALTServerError.h"
#import "CFNotificationName+AltStore.h"
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@interface AKDevice : NSObject @interface AKDevice : NSObject

View File

@@ -9,8 +9,6 @@
import Foundation import Foundation
import Network import Network
import AltKit
private let ReceivedLocalServerConnectionRequest: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = private let ReceivedLocalServerConnectionRequest: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
{ (center, observer, name, object, userInfo) in { (center, observer, name, object, userInfo) in
guard let name = name, let observer = observer else { return } guard let name = name, let observer = observer else { return }

View File

@@ -7,21 +7,20 @@
// //
import Foundation 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()]) connectionHandlers: [LocalConnectionHandler()])
extension ConnectionManager extension DaemonConnectionManager
{ {
static var shared: ConnectionManager { static var shared: ConnectionManager {
return connectionManager return connectionManager
} }
} }
struct RequestHandler: AltKit.RequestHandler struct DaemonRequestHandler: RequestHandler
{ {
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void) func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void)
{ {

View File

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

View File

@@ -5,4 +5,9 @@
#import "ALTDeviceManager.h" #import "ALTDeviceManager.h"
#import "ALTWiredConnection.h" #import "ALTWiredConnection.h"
#import "ALTNotificationConnection.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 Foundation
import AltKit
class AnisetteDataManager: NSObject class AnisetteDataManager: NSObject
{ {

View File

@@ -63,7 +63,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
ConnectionManager.shared.start() ServerConnectionManager.shared.start()
ALTDeviceManager.shared.start() ALTDeviceManager.shared.start()
let item = NSStatusBar.system.statusItem(withLength: -1) let item = NSStatusBar.system.statusItem(withLength: -1)

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,21 +7,20 @@
// //
import Foundation 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()]) connectionHandlers: [WirelessConnectionHandler(), WiredConnectionHandler()])
extension ConnectionManager extension ServerConnectionManager
{ {
static var shared: ConnectionManager { static var shared: ConnectionManager {
return connectionManager return connectionManager
} }
} }
struct RequestHandler: AltKit.RequestHandler struct ServerRequestHandler: RequestHandler
{ {
func handleAnisetteDataRequest(_ request: AnisetteDataRequest, for connection: Connection, completionHandler: @escaping (Result<AnisetteDataResponse, Error>) -> Void) 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 let progress = ALTDeviceManager.shared.installApp(at: fileURL, toDeviceWithUDID: udid, activeProvisioningProfiles: activeProvisioningProfiles) { (success, error) in
print("Installed app with result:", error == nil ? "Success" : error!.localizedDescription) 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)) completionHandler(.failure(error))
} }

View File

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

View File

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

View File

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

View File

@@ -8,10 +8,12 @@
#import "ALTDeviceManager.h" #import "ALTDeviceManager.h"
#import "AltKit.h"
#import "ALTWiredConnection+Private.h" #import "ALTWiredConnection+Private.h"
#import "ALTNotificationConnection+Private.h" #import "ALTNotificationConnection+Private.h"
#import "ALTConstants.h"
#import "NSError+ALTServerError.h"
#include <libimobiledevice/libimobiledevice.h> #include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h> #include <libimobiledevice/lockdown.h>
#include <libimobiledevice/installation_proxy.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"> location = "container:AltStore.xcodeproj">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:Dependencies/AltSign/AltSign.xcodeproj"> location = "group:Dependencies/AltSign">
</FileRef> </FileRef>
<FileRef <FileRef
location = "group:Dependencies/Roxas/Roxas.xcodeproj"> 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. // 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" #import "NSAttributedString+Markdown.h"

View File

@@ -4,6 +4,8 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.rileytestut.AltStore</string> <string>group.com.rileytestut.AltStore</string>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,47 +9,12 @@
import UIKit import UIKit
import UserNotifications import UserNotifications
import AVFoundation import AVFoundation
import Intents
import AltStoreCore
import AltSign import AltSign
import AltKit
import Roxas 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 extension AppDelegate
{ {
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification") static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
@@ -68,11 +33,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
private var runningApplications: Set<String>? private lazy var intentHandler = IntentHandler()
private var backgroundRefreshContext: NSManagedObjectContext? // Keep context alive until finished refreshing.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool 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() AnalyticsManager.shared.start()
self.setTintColor() self.setTintColor()
@@ -117,6 +92,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
{ {
return self.open(url) 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 private extension AppDelegate
@@ -234,93 +233,92 @@ extension AppDelegate
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void) 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 content = UNMutableNotificationContent()
{ content.title = NSLocalizedString("App Refresh Tip", comment: "")
let threeHours: TimeInterval = 3 * 60 * 60 content.body = NSLocalizedString("The more you open AltStore, the more chances it's given to refresh apps in the background.", comment: "")
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: threeHours, repeats: false)
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
let content = UNMutableNotificationContent() UNUserNotificationCenter.current().add(request)
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: "") UserDefaults.standard.presentedLaunchReminderNotification = true
let request = UNNotificationRequest(identifier: "background-refresh-reminder5", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request)
UserDefaults.standard.presentedLaunchReminderNotification = true
}
} }
let refreshIdentifier = UUID().uuidString
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in 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 if let error = taskResult.error
{ {
print("Error starting extended background task. Aborting.", error) print("Error starting extended background task. Aborting.", error)
backgroundFetchCompletionHandler(.failed) backgroundFetchCompletionHandler(.failed)
finish(.failure(error)) taskCompletionHandler()
return return
} }
if !DatabaseManager.shared.isStarted if !DatabaseManager.shared.isStarted
{ {
DatabaseManager.shared.start() { (error) in DatabaseManager.shared.start() { (error) in
if let error = error if error != nil
{ {
backgroundFetchCompletionHandler(.failed) backgroundFetchCompletionHandler(.failed)
finish(.failure(error)) taskCompletionHandler()
} }
else else
{ {
self.refreshApps(identifier: refreshIdentifier, backgroundFetchCompletionHandler: backgroundFetchCompletionHandler, completionHandler: finish(_:)) self.performBackgroundFetch { (backgroundFetchResult) in
backgroundFetchCompletionHandler(backgroundFetchResult)
} refreshAppsCompletionHandler: { (refreshAppsResult) in
taskCompletionHandler()
}
} }
} }
} }
else 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 private extension AppDelegate
{ {
func refreshApps(identifier: String, func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
{ {
var fetchSourcesResult: Result<Set<Source>, Error>?
var serversResult: Result<Void, Error>?
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
AppManager.shared.fetchSources() { (result) in AppManager.shared.fetchSources() { (result) in
fetchSourcesResult = result.map { $0.0 }.mapError { $0 as Error }
do do
{ {
let (_, context) = try result.get() let (sources, context) = try result.get()
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult> let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
previousUpdatesFetchRequest.includesPendingChanges = false previousUpdatesFetchRequest.includesPendingChanges = false
@@ -383,223 +381,14 @@ private extension AppDelegate
DispatchQueue.main.async { DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = updates.count UIApplication.shared.applicationIconBadgeNumber = updates.count
} }
completionHandler(.success(sources))
} }
catch catch
{ {
print("Error fetching apps:", error) print("Error fetching apps:", error)
completionHandler(.failure(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))
}
}
}
} }
} }
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 UIKit
import AltSign
import AltStoreCore
import AltSign
import Roxas import Roxas
class RefreshAltStoreViewController: UIViewController class RefreshAltStoreViewController: UIViewController

View File

@@ -8,6 +8,7 @@
import UIKit import UIKit
import AltStoreCore
import Roxas import Roxas
import Nuke import Nuke
@@ -83,6 +84,7 @@ private extension BrowseViewController
cell.layoutMargins.left = self.view.layoutMargins.left cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right cell.layoutMargins.right = self.view.layoutMargins.right
cell.subtitleLabel.text = app.subtitle
cell.imageURLs = Array(app.screenshotURLs.prefix(2)) cell.imageURLs = Array(app.screenshotURLs.prefix(2))
cell.bannerView.configure(for: app) cell.bannerView.configure(for: app)

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
import Foundation import Foundation
import AltKit import AltStoreCore
extension FileManager 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> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>INIntentsSupported</key>
<array>
<string>RefreshAllIntent</string>
</array>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>altstore-com.rileytestut.AltStore</string> <string>altstore-com.rileytestut.AltStore</string>
@@ -85,6 +89,31 @@
</array> </array>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>AltStore uses the local network to find and communicate with AltServer.</string> <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> <key>UIBackgroundModes</key>
<array> <array>
<string>audio</string> <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 UIKit
import Roxas import Roxas
import AltStoreCore
class LaunchViewController: RSTLaunchViewController class LaunchViewController: RSTLaunchViewController
{ {
private var didFinishLaunching = false private var didFinishLaunching = false

View File

@@ -10,10 +10,11 @@ import Foundation
import UIKit import UIKit
import UserNotifications import UserNotifications
import MobileCoreServices import MobileCoreServices
import Intents
import Combine
import AltStoreCore
import AltSign import AltSign
import AltKit
import Roxas import Roxas
extension AppManager extension AppManager
@@ -23,15 +24,41 @@ extension AppManager
static let expirationWarningNotificationID = "altstore-expiration-warning" 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 class AppManager
{ {
static let shared = AppManager() static let shared = AppManager()
@available(iOS 13, *)
private(set) lazy var publisher: AppManagerPublisher = AppManagerPublisher()
private let operationQueue = OperationQueue() private let operationQueue = OperationQueue()
private let serialOperationQueue = OperationQueue() private let serialOperationQueue = OperationQueue()
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
}
}
private var installationProgress = [String: Progress]() @available(iOS 13.0, *)
private var refreshProgress = [String: Progress]() private lazy var cancellables = Set<AnyCancellable>()
private init() private init()
{ {
@@ -39,6 +66,28 @@ class AppManager
self.serialOperationQueue.name = "com.altstore.AppManager.serialOperationQueue" self.serialOperationQueue.name = "com.altstore.AppManager.serialOperationQueue"
self.serialOperationQueue.maxConcurrentOperationCount = 1 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 private extension AppManager
{ {
enum AppOperation enum AppOperation

View File

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

View File

@@ -8,11 +8,11 @@
import UIKit import UIKit
import MobileCoreServices import MobileCoreServices
import Intents
import AltKit import AltStoreCore
import Roxas
import AltSign import AltSign
import Roxas
import Nuke import Nuke
@@ -654,6 +654,15 @@ private extension MyAppsViewController
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue]) 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) @IBAction func updateApp(_ sender: UIButton)

View File

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

View File

@@ -10,7 +10,7 @@ import Foundation
import Roxas import Roxas
import Network import Network
import AltKit import AltStoreCore
import AltSign import AltSign
enum AuthenticationError: LocalizedError 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 Foundation
import AltKit import AltStoreCore
import AltSign import AltSign
extension BackupAppOperation extension BackupAppOperation

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@
import Foundation import Foundation
import CoreData import CoreData
import AltStoreCore
import Roxas import Roxas
@objc(FetchSourceOperation) @objc(FetchSourceOperation)
@@ -49,7 +50,7 @@ class FetchSourceOperation: ResultOperation<Source>
{ {
let (data, _) = try Result((data, response), error).get() let (data, _) = try Result((data, response), error).get()
let decoder = JSONDecoder() let decoder = AltStoreCore.JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
let text = try container.decode(String.self) let text = try container.decode(String.self)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,6 @@
import Foundation import Foundation
import AltSign import AltSign
import AltKit
import Roxas import Roxas
enum VerificationError: ALTLocalizedError 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 Network
import AltKit
enum ConnectionError: LocalizedError enum ConnectionError: LocalizedError
{ {
case serverNotFound case serverNotFound

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
import Network import Network
import AltKit import AltStoreCore
class ServerConnection class ServerConnection
{ {
@@ -95,7 +95,7 @@ class ServerConnection
{ {
let data = try self.process(data: data, error: error) 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)) completionHandler(.success(response))
} }
catch catch

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <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="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.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="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" 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"> <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"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -74,7 +74,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </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"> <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"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -106,7 +106,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </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"> <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"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -138,7 +138,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </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"> <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"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -167,7 +167,7 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <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"> <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"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -207,7 +207,7 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <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"> <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"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -230,17 +230,45 @@
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/> <edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes> <userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style"> <userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="0"/> <integer key="value" value="1"/>
</userDefinedRuntimeAttribute> </userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/> <userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </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> </cells>
</tableViewSection> </tableViewSection>
<tableViewSection headerTitle="" id="9ht-ML-85l"> <tableViewSection headerTitle="" id="9ht-ML-85l">
<cells> <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"> <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"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -270,14 +298,14 @@
<tableViewSection headerTitle="" id="eHy-qI-w5w"> <tableViewSection headerTitle="" id="eHy-qI-w5w">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="30h-59-88f" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target"> <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"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <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"> <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"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -310,7 +338,7 @@
<tableViewSection headerTitle="" id="J90-vn-u2O"> <tableViewSection headerTitle="" id="J90-vn-u2O">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="i4T-2q-jF3" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target"> <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"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -323,16 +351,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk"> <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> <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"> <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"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb"> <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> </imageView>
</subviews> </subviews>
</stackView> </stackView>
@@ -354,7 +382,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="0MT-ht-Sit" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target"> <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"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -367,16 +395,16 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY"> <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> <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"> <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"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY"> <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> </imageView>
</subviews> </subviews>
</stackView> </stackView>
@@ -398,7 +426,7 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="O5R-Al-lGj" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target"> <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"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -438,7 +466,7 @@
<tableViewSection headerTitle="" id="OMa-EK-hRI"> <tableViewSection headerTitle="" id="OMa-EK-hRI">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="FMZ-as-Ljo" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target"> <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"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
@@ -471,14 +499,14 @@
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="Qca-pU-sJh" customClass="InsetGroupTableViewCell" customModule="AltStore" customModuleProvider="target"> <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"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <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"> <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"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
@@ -581,9 +609,9 @@
<rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/> <rect key="frame" x="0.0" y="0.0" width="343" height="20.5"/>
<subviews> <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"> <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"/> <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/> <color key="textColor" systemColor="darkTextColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </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"> <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"/> <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView> </textView>
</subviews> </subviews>
<viewLayoutGuide key="safeArea" id="o3f-Lj-IHF"/>
<color key="backgroundColor" name="SettingsBackground"/> <color key="backgroundColor" name="SettingsBackground"/>
<constraints> <constraints>
<constraint firstItem="oQQ-pR-oKc" firstAttribute="top" secondItem="o3f-Lj-IHF" secondAttribute="top" id="3gx-wh-Lol"/> <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="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"/> <constraint firstItem="oQQ-pR-oKc" firstAttribute="trailing" secondItem="o3f-Lj-IHF" secondAttribute="trailing" id="oUR-b9-ajN"/>
</constraints> </constraints>
<viewLayoutGuide key="safeArea" id="o3f-Lj-IHF"/>
</view> </view>
<navigationItem key="navigationItem" title="Software Licenses" largeTitleDisplayMode="never" id="JcT-wX-zay"/> <navigationItem key="navigationItem" title="Software Licenses" largeTitleDisplayMode="never" id="JcT-wX-zay"/>
<connections> <connections>
@@ -807,5 +835,8 @@ Settings by i cons from the Noun Project</string>
<namedColor name="SettingsBackground"> <namedColor name="SettingsBackground">
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor> </namedColor>
<systemColor name="darkTextColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources> </resources>
</document> </document>

View File

@@ -9,6 +9,10 @@
import UIKit import UIKit
import SafariServices import SafariServices
import MessageUI import MessageUI
import Intents
import IntentsUI
import AltStoreCore
extension SettingsViewController extension SettingsViewController
{ {
@@ -17,13 +21,26 @@ extension SettingsViewController
case signIn case signIn
case account case account
case patreon case patreon
case backgroundRefresh case appRefresh
case jailbreak case jailbreak
case instructions case instructions
case credits case credits
case debug 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 fileprivate enum CreditsRow: Int, CaseIterable
{ {
case developer case developer
@@ -165,8 +182,15 @@ private extension SettingsViewController
settingsHeaderFooterView.button.addTarget(self, action: #selector(SettingsViewController.signOut(_:)), for: .primaryActionTriggered) settingsHeaderFooterView.button.addTarget(self, action: #selector(SettingsViewController.signOut(_:)), for: .primaryActionTriggered)
settingsHeaderFooterView.button.isHidden = false settingsHeaderFooterView.button.isHidden = false
case .backgroundRefresh: case .appRefresh:
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Automatically refresh apps in the background when connected to the same WiFi as AltServer.", comment: "") 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: case .jailbreak:
if isHeader if isHeader
@@ -254,6 +278,17 @@ private extension SettingsViewController
UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn 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) @IBAction func handleDebugModeGesture(_ gestureRecognizer: UISwipeGestureRecognizer)
{ {
self.debugGestureCounter += 1 self.debugGestureCounter += 1
@@ -331,11 +366,28 @@ extension SettingsViewController
{ {
case .signIn: return (self.activeTeam == nil) ? 1 : 0 case .signIn: return (self.activeTeam == nil) ? 1 : 0
case .account: return (self.activeTeam == nil) ? 0 : 3 case .account: return (self.activeTeam == nil) ? 0 : 3
case .appRefresh: return AppRefreshRow.allCases.count
case .jailbreak: return UIDevice.current.isJailbroken ? 1 : 0 case .jailbreak: return UIDevice.current.isJailbroken ? 1 : 0
default: return super.tableView(tableView, numberOfRowsInSection: section.rawValue) 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? override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{ {
let section = Section.allCases[section] let section = Section.allCases[section]
@@ -345,12 +397,12 @@ extension SettingsViewController
case .account where self.activeTeam == nil: return nil case .account where self.activeTeam == nil: return nil
case .jailbreak where !UIDevice.current.isJailbroken: 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 let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(headerView, for: section, isHeader: true) self.prepare(headerView, for: section, isHeader: true)
return headerView 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 .signIn where self.activeTeam != nil: return nil
case .jailbreak where !UIDevice.current.isJailbroken: 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 let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(footerView, for: section, isHeader: false) self.prepare(footerView, for: section, isHeader: false)
return footerView return footerView
@@ -380,11 +432,11 @@ extension SettingsViewController
case .account where self.activeTeam == nil: return 1.0 case .account where self.activeTeam == nil: return 1.0
case .jailbreak where !UIDevice.current.isJailbroken: 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) let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
return height 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 .account where self.activeTeam == nil: return 1.0
case .jailbreak where !UIDevice.current.isJailbroken: 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) let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
return height return height
@@ -415,6 +467,16 @@ extension SettingsViewController
{ {
case .signIn: self.signIn() case .signIn: self.signIn()
case .instructions: break 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: case .jailbreak:
let fileURL = Bundle.main.url(forResource: "AltDaemon", withExtension: "deb")! let fileURL = Bundle.main.url(forResource: "AltDaemon", withExtension: "deb")!
@@ -489,3 +551,31 @@ extension SettingsViewController: UIGestureRecognizerDelegate
return true 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 UIKit
import CoreData import CoreData
import AltStoreCore
import Roxas import Roxas
class SourcesViewController: UICollectionViewController 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 import AltSign
@propertyWrapper @propertyWrapper
struct KeychainItem<Value> public struct KeychainItem<Value>
{ {
let key: String public let key: String
var wrappedValue: Value? { public var wrappedValue: Value? {
get { get {
switch Value.self switch Value.self
{ {
@@ -35,50 +35,50 @@ struct KeychainItem<Value>
} }
} }
init(key: String) public init(key: String)
{ {
self.key = key 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) fileprivate let keychain = KeychainAccess.Keychain(service: "com.rileytestut.AltStore").accessibility(.afterFirstUnlock).synchronizable(true)
@KeychainItem(key: "appleIDEmailAddress") @KeychainItem(key: "appleIDEmailAddress")
var appleIDEmailAddress: String? public var appleIDEmailAddress: String?
@KeychainItem(key: "appleIDPassword") @KeychainItem(key: "appleIDPassword")
var appleIDPassword: String? public var appleIDPassword: String?
@KeychainItem(key: "signingCertificatePrivateKey") @KeychainItem(key: "signingCertificatePrivateKey")
var signingCertificatePrivateKey: Data? public var signingCertificatePrivateKey: Data?
@KeychainItem(key: "signingCertificateSerialNumber") @KeychainItem(key: "signingCertificateSerialNumber")
var signingCertificateSerialNumber: String? public var signingCertificateSerialNumber: String?
@KeychainItem(key: "signingCertificate") @KeychainItem(key: "signingCertificate")
var signingCertificate: Data? public var signingCertificate: Data?
@KeychainItem(key: "signingCertificatePassword") @KeychainItem(key: "signingCertificatePassword")
var signingCertificatePassword: String? public var signingCertificatePassword: String?
@KeychainItem(key: "patreonAccessToken") @KeychainItem(key: "patreonAccessToken")
var patreonAccessToken: String? public var patreonAccessToken: String?
@KeychainItem(key: "patreonRefreshToken") @KeychainItem(key: "patreonRefreshToken")
var patreonRefreshToken: String? public var patreonRefreshToken: String?
@KeychainItem(key: "patreonCreatorAccessToken") @KeychainItem(key: "patreonCreatorAccessToken")
var patreonCreatorAccessToken: String? public var patreonCreatorAccessToken: String?
private init() private init()
{ {
} }
func reset() public func reset()
{ {
self.appleIDEmailAddress = nil self.appleIDEmailAddress = nil
self.appleIDPassword = nil self.appleIDPassword = nil

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
import CoreData import CoreData
extension CodingUserInfoKey public extension CodingUserInfoKey
{ {
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")! static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
static let sourceURL = CodingUserInfoKey(rawValue: "sourceURL")! static let sourceURL = CodingUserInfoKey(rawValue: "sourceURL")!
@@ -18,29 +18,29 @@ extension CodingUserInfoKey
public final class JSONDecoder: Foundation.JSONDecoder public final class JSONDecoder: Foundation.JSONDecoder
{ {
@DecoderItem(key: .managedObjectContext) @DecoderItem(key: .managedObjectContext)
var managedObjectContext: NSManagedObjectContext? public var managedObjectContext: NSManagedObjectContext?
@DecoderItem(key: .sourceURL) @DecoderItem(key: .sourceURL)
var sourceURL: URL? public var sourceURL: URL?
} }
extension Decoder public extension Decoder
{ {
var managedObjectContext: NSManagedObjectContext? { self.userInfo[.managedObjectContext] as? NSManagedObjectContext } var managedObjectContext: NSManagedObjectContext? { self.userInfo[.managedObjectContext] as? NSManagedObjectContext }
var sourceURL: URL? { self.userInfo[.sourceURL] as? URL } var sourceURL: URL? { self.userInfo[.sourceURL] as? URL }
} }
@propertyWrapper @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") } get { fatalError("only works on instance properties of classes") }
set { 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 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 import UIKit
extension UIColor public extension UIColor
{ {
// Borrowed from https://stackoverflow.com/a/26341062 // Borrowed from https://stackoverflow.com/a/26341062
var hexString: String { var hexString: String {

View File

@@ -10,7 +10,7 @@ import Foundation
import Roxas import Roxas
extension UserDefaults public extension UserDefaults
{ {
@NSManaged var firstLaunch: Date? @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 import AltSign
@objc(Account) @objc(Account)
class Account: NSManagedObject, Fetchable public class Account: NSManagedObject, Fetchable
{ {
var localizedName: String { public var localizedName: String {
var components = PersonNameComponents() var components = PersonNameComponents()
components.givenName = self.firstName components.givenName = self.firstName
components.familyName = self.lastName components.familyName = self.lastName
@@ -24,30 +24,30 @@ class Account: NSManagedObject, Fetchable
} }
/* Properties */ /* Properties */
@NSManaged var appleID: String @NSManaged public var appleID: String
@NSManaged var identifier: String @NSManaged public var identifier: String
@NSManaged var firstName: String @NSManaged public var firstName: String
@NSManaged var lastName: String @NSManaged public var lastName: String
@NSManaged var isActiveAccount: Bool @NSManaged public var isActiveAccount: Bool
/* Relationships */ /* Relationships */
@NSManaged var teams: Set<Team> @NSManaged public var teams: Set<Team>
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{ {
super.init(entity: entity, insertInto: context) super.init(entity: entity, insertInto: context)
} }
init(_ account: ALTAccount, context: NSManagedObjectContext) public init(_ account: ALTAccount, context: NSManagedObjectContext)
{ {
super.init(entity: Account.entity(), insertInto: context) super.init(entity: Account.entity(), insertInto: context)
self.update(account: account) self.update(account: account)
} }
func update(account: ALTAccount) public func update(account: ALTAccount)
{ {
self.appleID = account.appleID self.appleID = account.appleID
self.identifier = account.identifier self.identifier = account.identifier
@@ -57,7 +57,7 @@ class Account: NSManagedObject, Fetchable
} }
} }
extension Account public extension Account
{ {
@nonobjc class func fetchRequest() -> NSFetchRequest<Account> @nonobjc class func fetchRequest() -> NSFetchRequest<Account>
{ {

View File

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

View File

@@ -171,4 +171,4 @@
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/> <element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/> <element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
</elements> </elements>
</model> </model>

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 import AltSign
@objc(AppID) @objc(AppID)
class AppID: NSManagedObject, Fetchable public class AppID: NSManagedObject, Fetchable
{ {
/* Properties */ /* Properties */
@NSManaged var name: String @NSManaged public var name: String
@NSManaged var identifier: String @NSManaged public var identifier: String
@NSManaged var bundleIdentifier: String @NSManaged public var bundleIdentifier: String
@NSManaged var features: [ALTFeature: Any] @NSManaged public var features: [ALTFeature: Any]
@NSManaged var expirationDate: Date? @NSManaged public var expirationDate: Date?
/* Relationships */ /* Relationships */
@NSManaged private(set) var team: Team? @NSManaged public private(set) var team: Team?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{ {
super.init(entity: entity, insertInto: context) 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) 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> @nonobjc class func fetchRequest() -> NSFetchRequest<AppID>
{ {

View File

@@ -9,7 +9,7 @@
import CoreData import CoreData
import UIKit import UIKit
extension ALTAppPermissionType public extension ALTAppPermissionType
{ {
var localizedShortName: String? { var localizedShortName: String? {
switch self switch self
@@ -43,14 +43,14 @@ extension ALTAppPermissionType
} }
@objc(AppPermission) @objc(AppPermission)
class AppPermission: NSManagedObject, Decodable, Fetchable public class AppPermission: NSManagedObject, Decodable, Fetchable
{ {
/* Properties */ /* Properties */
@NSManaged var type: ALTAppPermissionType @NSManaged public var type: ALTAppPermissionType
@NSManaged var usageDescription: String @NSManaged public var usageDescription: String
/* Relationships */ /* Relationships */
@NSManaged private(set) var app: StoreApp! @NSManaged public private(set) var app: StoreApp!
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{ {
@@ -63,7 +63,7 @@ class AppPermission: NSManagedObject, Decodable, Fetchable
case usageDescription 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.") } 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> @nonobjc class func fetchRequest() -> NSFetchRequest<AppPermission>
{ {

View File

@@ -20,10 +20,11 @@ public class DatabaseManager
public private(set) var isStarted = false public private(set) var isStarted = false
private var startCompletionHandlers = [(Error?) -> Void]() private var startCompletionHandlers = [(Error?) -> Void]()
private let dispatchQueue = DispatchQueue(label: "io.altstore.DatabaseManager")
private init() private init()
{ {
self.persistentContainer = RSTPersistentContainer(name: "AltStore") self.persistentContainer = RSTPersistentContainer(name: "AltStore", bundle: Bundle(for: DatabaseManager.self))
self.persistentContainer.preferredMergePolicy = MergePolicy() self.persistentContainer.preferredMergePolicy = MergePolicy()
} }
} }
@@ -32,30 +33,34 @@ public extension DatabaseManager
{ {
func start(completionHandler: @escaping (Error?) -> Void) func start(completionHandler: @escaping (Error?) -> Void)
{ {
self.startCompletionHandlers.append(completionHandler)
guard self.startCompletionHandlers.count == 1 else { return }
func finish(_ error: Error?) func finish(_ error: Error?)
{ {
self.startCompletionHandlers.forEach { $0(error) } self.dispatchQueue.async {
self.startCompletionHandlers.removeAll() 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)
self.persistentContainer.loadPersistentStores { (description, error) in guard self.startCompletionHandlers.count == 1 else { return }
guard error == nil else { return finish(error!) }
self.prepareDatabase() { (result) in guard !self.isStarted else { return finish(nil) }
switch result
{ self.persistentContainer.loadPersistentStores { (description, error) in
case .failure(let error): guard error == nil else { return finish(error!) }
finish(error)
self.prepareDatabase() { (result) in
case .success: switch result
self.isStarted = true {
finish(nil) 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? func activeAccount(in context: NSManagedObjectContext = DatabaseManager.shared.viewContext) -> Account?
{ {

View File

@@ -12,9 +12,9 @@ import CoreData
import AltSign import AltSign
// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1. // 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 name: String { get }
var bundleIdentifier: String { get } var bundleIdentifier: String { get }
@@ -27,36 +27,39 @@ protocol InstalledAppProtocol: Fetchable
} }
@objc(InstalledApp) @objc(InstalledApp)
class InstalledApp: NSManagedObject, InstalledAppProtocol public class InstalledApp: NSManagedObject, InstalledAppProtocol
{ {
/* Properties */ /* Properties */
@NSManaged var name: String @NSManaged public var name: String
@NSManaged var bundleIdentifier: String @NSManaged public var bundleIdentifier: String
@NSManaged var resignedBundleIdentifier: String @NSManaged public var resignedBundleIdentifier: String
@NSManaged var version: String @NSManaged public var version: String
@NSManaged var refreshedDate: Date @NSManaged public var refreshedDate: Date
@NSManaged var expirationDate: Date @NSManaged public var expirationDate: Date
@NSManaged var installedDate: 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 */ /* Relationships */
@NSManaged var storeApp: StoreApp? @NSManaged public var storeApp: StoreApp?
@NSManaged var team: Team? @NSManaged public var team: Team?
@NSManaged var appExtensions: Set<InstalledExtension> @NSManaged public var appExtensions: Set<InstalledExtension>
var isSideloaded: Bool { public var isSideloaded: Bool {
return self.storeApp == nil return self.storeApp == nil
} }
var appIDCount: Int { public var appIDCount: Int {
return 1 + self.appExtensions.count return 1 + self.appExtensions.count
} }
var requiredActiveSlots: Int { public var requiredActiveSlots: Int {
let requiredActiveSlots = UserDefaults.standard.activeAppLimitIncludesExtensions ? self.appIDCount : 1 let requiredActiveSlots = UserDefaults.standard.activeAppLimitIncludesExtensions ? self.appIDCount : 1
return requiredActiveSlots return requiredActiveSlots
} }
@@ -66,7 +69,7 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
super.init(entity: entity, insertInto: context) 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) super.init(entity: InstalledApp.entity(), insertInto: context)
@@ -80,7 +83,7 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber) self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber)
} }
func update(resignedApp: ALTApplication, certificateSerialNumber: String?) public func update(resignedApp: ALTApplication, certificateSerialNumber: String?)
{ {
self.name = resignedApp.name 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.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate self.expirationDate = provisioningProfile.expirationDate
} }
} }
extension InstalledApp public extension InstalledApp
{ {
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledApp> @nonobjc class func fetchRequest() -> NSFetchRequest<InstalledApp>
{ {
@@ -199,7 +202,7 @@ extension InstalledApp
} }
} }
extension InstalledApp public extension InstalledApp
{ {
var openAppURL: URL { var openAppURL: URL {
let openAppURL = URL(string: "altstore-" + self.bundleIdentifier + "://")! let openAppURL = URL(string: "altstore-" + self.bundleIdentifier + "://")!
@@ -213,7 +216,7 @@ extension InstalledApp
} }
} }
extension InstalledApp public extension InstalledApp
{ {
class var appsDirectoryURL: URL { class var appsDirectoryURL: URL {
let appsDirectoryURL = FileManager.default.applicationSupportDirectory.appendingPathComponent("Apps") let appsDirectoryURL = FileManager.default.applicationSupportDirectory.appendingPathComponent("Apps")

View File

@@ -12,27 +12,27 @@ import CoreData
import AltSign import AltSign
@objc(InstalledExtension) @objc(InstalledExtension)
class InstalledExtension: NSManagedObject, InstalledAppProtocol public class InstalledExtension: NSManagedObject, InstalledAppProtocol
{ {
/* Properties */ /* Properties */
@NSManaged var name: String @NSManaged public var name: String
@NSManaged var bundleIdentifier: String @NSManaged public var bundleIdentifier: String
@NSManaged var resignedBundleIdentifier: String @NSManaged public var resignedBundleIdentifier: String
@NSManaged var version: String @NSManaged public var version: String
@NSManaged var refreshedDate: Date @NSManaged public var refreshedDate: Date
@NSManaged var expirationDate: Date @NSManaged public var expirationDate: Date
@NSManaged var installedDate: Date @NSManaged public var installedDate: Date
/* Relationships */ /* Relationships */
@NSManaged var parentApp: InstalledApp? @NSManaged public var parentApp: InstalledApp?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{ {
super.init(entity: entity, insertInto: context) 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) super.init(entity: InstalledExtension.entity(), insertInto: context)
@@ -46,7 +46,7 @@ class InstalledExtension: NSManagedObject, InstalledAppProtocol
self.update(resignedAppExtension: resignedAppExtension) self.update(resignedAppExtension: resignedAppExtension)
} }
func update(resignedAppExtension: ALTApplication) public func update(resignedAppExtension: ALTApplication)
{ {
self.name = resignedAppExtension.name 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.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate self.expirationDate = provisioningProfile.expirationDate
} }
} }
extension InstalledExtension public extension InstalledExtension
{ {
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledExtension> @nonobjc class func fetchRequest() -> NSFetchRequest<InstalledExtension>
{ {

Some files were not shown because too many files have changed in this diff Show More