mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[AltStore] Tracks background refresh attempts for debugging
This commit is contained in:
@@ -11,6 +11,8 @@
|
|||||||
BF0201BB22C2EFA3000B93E4 /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
BF0201BB22C2EFA3000B93E4 /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
BF0201BD22C2EFBC000B93E4 /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; };
|
BF0201BD22C2EFBC000B93E4 /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; };
|
||||||
BF0201BE22C2EFBC000B93E4 /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
BF0201BE22C2EFBC000B93E4 /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
|
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02419322F2156E00129732 /* RefreshAttempt.swift */; };
|
||||||
|
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02419522F2199300129732 /* RefreshAttemptsViewController.swift */; };
|
||||||
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858222DE795100DE9F1E /* MyAppsViewController.swift */; };
|
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858222DE795100DE9F1E /* MyAppsViewController.swift */; };
|
||||||
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */; };
|
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */; };
|
||||||
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */; };
|
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */; };
|
||||||
@@ -252,6 +254,8 @@
|
|||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
1039C07E517311FC499A0B64 /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
1039C07E517311FC499A0B64 /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
A136EE677716B80768E9F0A2 /* Pods-AltStore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.release.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.release.xcconfig"; sourceTree = "<group>"; };
|
A136EE677716B80768E9F0A2 /* Pods-AltStore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.release.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
BF02419322F2156E00129732 /* RefreshAttempt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAttempt.swift; sourceTree = "<group>"; };
|
||||||
|
BF02419522F2199300129732 /* RefreshAttemptsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAttemptsViewController.swift; sourceTree = "<group>"; };
|
||||||
BF08858222DE795100DE9F1E /* MyAppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewController.swift; sourceTree = "<group>"; };
|
BF08858222DE795100DE9F1E /* MyAppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewController.swift; sourceTree = "<group>"; };
|
||||||
BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCollectionViewCell.swift; sourceTree = "<group>"; };
|
BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = "<group>"; };
|
BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = "<group>"; };
|
||||||
@@ -853,6 +857,7 @@
|
|||||||
BFBBE2DE22931F73002097FA /* App.swift */,
|
BFBBE2DE22931F73002097FA /* App.swift */,
|
||||||
BF3D648722E79A3700E9056B /* AppPermission.swift */,
|
BF3D648722E79A3700E9056B /* AppPermission.swift */,
|
||||||
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
|
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
|
||||||
|
BF02419322F2156E00129732 /* RefreshAttempt.swift */,
|
||||||
BFE338DC22F0E7F3002E24B9 /* Source.swift */,
|
BFE338DC22F0E7F3002E24B9 /* Source.swift */,
|
||||||
BFE6326522A857C100F30809 /* Team.swift */,
|
BFE6326522A857C100F30809 /* Team.swift */,
|
||||||
);
|
);
|
||||||
@@ -883,6 +888,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
BFDB69FC22A9A7B7007EA6D6 /* SettingsViewController.swift */,
|
BFDB69FC22A9A7B7007EA6D6 /* SettingsViewController.swift */,
|
||||||
|
BF02419522F2199300129732 /* RefreshAttemptsViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Settings;
|
path = Settings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1300,6 +1306,7 @@
|
|||||||
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */,
|
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */,
|
||||||
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
||||||
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
||||||
|
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
|
||||||
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
|
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
|
||||||
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */,
|
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */,
|
||||||
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
|
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
|
||||||
@@ -1312,6 +1319,7 @@
|
|||||||
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */,
|
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */,
|
||||||
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
|
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
|
||||||
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
|
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
|
||||||
|
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
|
||||||
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
|
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
|
||||||
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */,
|
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */,
|
||||||
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
|
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ import AVFoundation
|
|||||||
import AltSign
|
import AltSign
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
|
private enum RefreshError: LocalizedError
|
||||||
|
{
|
||||||
|
case noInstalledApps
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self
|
||||||
|
{
|
||||||
|
case .noInstalledApps: return NSLocalizedString("No installed apps to refresh.", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private extension CFNotificationName
|
private extension CFNotificationName
|
||||||
{
|
{
|
||||||
static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString)
|
static let requestAppState = CFNotificationName("com.altstore.RequestAppState" as CFString)
|
||||||
@@ -43,12 +55,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
private var runningApplications: Set<String>?
|
private var runningApplications: Set<String>?
|
||||||
private var isLaunching = false
|
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||||
{
|
{
|
||||||
self.isLaunching = true
|
|
||||||
|
|
||||||
self.setTintColor()
|
self.setTintColor()
|
||||||
|
|
||||||
ServerManager.shared.startDiscovering()
|
ServerManager.shared.startDiscovering()
|
||||||
@@ -61,10 +70,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
|
|
||||||
self.prepareForBackgroundFetch()
|
self.prepareForBackgroundFetch()
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isLaunching = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,36 +123,12 @@ extension AppDelegate
|
|||||||
self.application(application, performFetchWithCompletionHandler: completionHandler)
|
self.application(application, performFetchWithCompletionHandler: completionHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
func application(_ application: UIApplication, performFetchWithCompletionHandler backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void)
|
||||||
{
|
{
|
||||||
let isLaunching = self.isLaunching
|
ServerManager.shared.startDiscovering()
|
||||||
|
|
||||||
let refreshIdentifier = UUID().uuidString
|
let refreshIdentifier = UUID().uuidString
|
||||||
|
|
||||||
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: DatabaseManager.shared.viewContext)
|
|
||||||
guard !installedApps.isEmpty else {
|
|
||||||
ServerManager.shared.stopDiscovering()
|
|
||||||
completionHandler(.noData)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.runningApplications = []
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
|
BackgroundTaskManager.shared.performExtendedBackgroundTask { (taskResult, taskCompletionHandler) in
|
||||||
|
|
||||||
func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>)
|
func finish(_ result: Result<[String: Result<InstalledApp, Error>], Error>)
|
||||||
@@ -156,26 +137,74 @@ extension AppDelegate
|
|||||||
|
|
||||||
ServerManager.shared.stopDiscovering()
|
ServerManager.shared.stopDiscovering()
|
||||||
|
|
||||||
self.scheduleFinishedRefreshingNotification(for: result, identifier: refreshIdentifier, isLaunching: isLaunching, delay: 0)
|
self.scheduleFinishedRefreshingNotification(for: result, identifier: refreshIdentifier, delay: 0)
|
||||||
|
|
||||||
taskCompletionHandler()
|
taskCompletionHandler()
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard UIApplication.shared.applicationState == .background else { return }
|
|
||||||
|
|
||||||
// Exit so that if background fetch occurs again soon we're not suspended.
|
|
||||||
exit(0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
completionHandler(.failed)
|
backgroundFetchCompletionHandler(.failed)
|
||||||
finish(.failure(error))
|
finish(.failure(error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !DatabaseManager.shared.isStarted
|
||||||
|
{
|
||||||
|
DatabaseManager.shared.start() { (error) in
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
|
backgroundFetchCompletionHandler(.failed)
|
||||||
|
finish(.failure(error))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.refreshApps(identifier: refreshIdentifier, backgroundFetchCompletionHandler: backgroundFetchCompletionHandler, completionHandler: finish(_:))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.refreshApps(identifier: refreshIdentifier, backgroundFetchCompletionHandler: backgroundFetchCompletionHandler, completionHandler: finish(_:))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppDelegate
|
||||||
|
{
|
||||||
|
func refreshApps(identifier: String,
|
||||||
|
backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||||
|
completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
||||||
|
{
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
|
|
||||||
|
let installedApps = InstalledApp.fetchAppsForBackgroundRefresh(in: context)
|
||||||
|
guard !installedApps.isEmpty else {
|
||||||
|
backgroundFetchCompletionHandler(.noData)
|
||||||
|
completionHandler(.failure(RefreshError.noInstalledApps))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.runningApplications = []
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var fetchSourceResult: Result<Source, Error>?
|
var fetchSourceResult: Result<Source, Error>?
|
||||||
var serversResult: Result<Void, Error>?
|
var serversResult: Result<Void, Error>?
|
||||||
|
|
||||||
@@ -231,16 +260,16 @@ extension AppDelegate
|
|||||||
|
|
||||||
dispatchGroup.notify(queue: .main) {
|
dispatchGroup.notify(queue: .main) {
|
||||||
guard let fetchSourceResult = fetchSourceResult, let serversResult = serversResult else {
|
guard let fetchSourceResult = fetchSourceResult, let serversResult = serversResult else {
|
||||||
completionHandler(.failed)
|
backgroundFetchCompletionHandler(.failed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call completionHandler early to improve chances of refreshing in the background again.
|
// Call completionHandler early to improve chances of refreshing in the background again.
|
||||||
switch (fetchSourceResult, serversResult)
|
switch (fetchSourceResult, serversResult)
|
||||||
{
|
{
|
||||||
case (.success, .success): completionHandler(.newData)
|
case (.success, .success): backgroundFetchCompletionHandler(.newData)
|
||||||
case (.success, .failure(ConnectionError.serverNotFound)): completionHandler(.newData)
|
case (.success, .failure(ConnectionError.serverNotFound)): backgroundFetchCompletionHandler(.newData)
|
||||||
case (.failure, _), (_, .failure): completionHandler(.failed)
|
case (.failure, _), (_, .failure): backgroundFetchCompletionHandler(.failed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,45 +277,47 @@ extension AppDelegate
|
|||||||
// a) give us time to discover AltServers
|
// a) give us time to discover AltServers
|
||||||
// b) give other processes a chance to respond to requestAppState notification
|
// b) give other processes a chance to respond to requestAppState notification
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||||
if ServerManager.shared.discoveredServers.isEmpty
|
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 == App.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.error
|
|
||||||
{
|
{
|
||||||
self.scheduleFinishedRefreshingNotification(for: .failure(error), identifier: refreshIdentifier, isLaunching: isLaunching)
|
serversResult = .failure(ConnectionError.serverNotFound)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var results = group.results
|
serversResult = .success(())
|
||||||
results[installedApp.bundleIdentifier] = .success(installedApp)
|
}
|
||||||
|
|
||||||
self.scheduleFinishedRefreshingNotification(for: .success(results), identifier: refreshIdentifier, isLaunching: isLaunching)
|
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 == App.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.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 = { (result) in
|
||||||
|
completionHandler(result)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
group.completionHandler = { (result) in
|
|
||||||
finish(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,69 +331,76 @@ extension AppDelegate
|
|||||||
self.runningApplications?.insert(appID)
|
self.runningApplications?.insert(appID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scheduleFinishedRefreshingNotification(for result: Result<[String: Result<InstalledApp, Error>], Error>, identifier: String, isLaunching: Bool, delay: TimeInterval = 5)
|
func scheduleFinishedRefreshingNotification(for result: Result<[String: Result<InstalledApp, Error>], Error>, identifier: String, delay: TimeInterval = 5)
|
||||||
{
|
{
|
||||||
self.cancelFinishedRefreshingNotification(identifier: identifier)
|
func scheduleFinishedRefreshingNotification()
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
|
||||||
|
|
||||||
var shouldPresentAlert = true
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
{
|
||||||
let results = try result.get()
|
self.cancelFinishedRefreshingNotification(identifier: identifier)
|
||||||
shouldPresentAlert = !results.isEmpty
|
|
||||||
|
|
||||||
for (_, result) in results
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
|
var shouldPresentAlert = true
|
||||||
|
|
||||||
|
do
|
||||||
{
|
{
|
||||||
guard case let .failure(error) = result else { continue }
|
let results = try result.get()
|
||||||
throw error
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
content.title = NSLocalizedString("Refreshed Apps", comment: "")
|
if shouldPresentAlert
|
||||||
content.body = NSLocalizedString("All apps have been refreshed.", comment: "")
|
|
||||||
}
|
|
||||||
catch let error as NSError where
|
|
||||||
(error.domain == NSOSStatusErrorDomain || error.domain == AVFoundationErrorDomain) &&
|
|
||||||
error.code == AVAudioSession.ErrorCode.cannotStartPlaying.rawValue &&
|
|
||||||
!isLaunching
|
|
||||||
{
|
|
||||||
// We can only start background audio when the app is being launched,
|
|
||||||
// and _not_ if it's already suspended in background.
|
|
||||||
// Since we are currently suspended in background and not launching, we'll just ignore the error.
|
|
||||||
|
|
||||||
shouldPresentAlert = false
|
|
||||||
}
|
|
||||||
catch ConnectionError.serverNotFound
|
|
||||||
{
|
|
||||||
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) {
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false)
|
||||||
// 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.
|
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
||||||
self.scheduleFinishedRefreshingNotification(for: result, identifier: identifier, isLaunching: isLaunching)
|
UNUserNotificationCenter.current().add(request)
|
||||||
|
|
||||||
|
if delay > 0
|
||||||
|
{
|
||||||
|
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
|
||||||
|
// 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.
|
||||||
|
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)
|
func cancelFinishedRefreshingNotification(identifier: String)
|
||||||
|
|||||||
@@ -693,6 +693,30 @@ World</string>
|
|||||||
</tableViewCell>
|
</tableViewCell>
|
||||||
</cells>
|
</cells>
|
||||||
</tableViewSection>
|
</tableViewSection>
|
||||||
|
<tableViewSection headerTitle="Debug" id="K7R-6x-gHl">
|
||||||
|
<cells>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="nQj-qq-PmI" style="IBUITableViewCellStyleDefault" id="8M3-mu-gRd">
|
||||||
|
<rect key="frame" x="0.0" y="299.5" width="375" height="44"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="8M3-mu-gRd" id="6TQ-nF-Rkl">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Background Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="nQj-qq-PmI">
|
||||||
|
<rect key="frame" x="16" y="0.0" width="324" height="43.5"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<connections>
|
||||||
|
<segue destination="RUB-gO-oQ6" kind="show" id="tkT-F7-PA4"/>
|
||||||
|
</connections>
|
||||||
|
</tableViewCell>
|
||||||
|
</cells>
|
||||||
|
</tableViewSection>
|
||||||
<tableViewSection footerTitle="" id="Yg2-vc-vLQ">
|
<tableViewSection footerTitle="" id="Yg2-vc-vLQ">
|
||||||
<cells/>
|
<cells/>
|
||||||
</tableViewSection>
|
</tableViewSection>
|
||||||
@@ -721,6 +745,82 @@ World</string>
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="1518" y="515"/>
|
<point key="canvasLocation" x="1518" y="515"/>
|
||||||
</scene>
|
</scene>
|
||||||
|
<!--Refresh Attempts-->
|
||||||
|
<scene sceneID="tCt-AY-k3Z">
|
||||||
|
<objects>
|
||||||
|
<tableViewController id="RUB-gO-oQ6" customClass="RefreshAttemptsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="g2y-h0-0xu">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<prototypes>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="bBs-eV-Rlj" customClass="RefreshAttemptTableViewCell">
|
||||||
|
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="bBs-eV-Rlj" id="YfI-8z-wCv">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="SFD-t6-Qk3">
|
||||||
|
<rect key="frame" x="16" y="11" width="343" height="22"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="gQk-PG-cjg">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="343" height="17"/>
|
||||||
|
<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="CZj-FM-9Qv">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="67.5" height="17"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||||
|
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Date" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0Md-jd-XXe">
|
||||||
|
<rect key="frame" x="312.5" y="0.0" width="30.5" height="17"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
|
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Could not connect to AltServer." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pwo-OM-Gm3">
|
||||||
|
<rect key="frame" x="0.0" y="21" width="343" height="1"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="SFD-t6-Qk3" firstAttribute="leading" secondItem="YfI-8z-wCv" secondAttribute="leadingMargin" id="ehs-Sf-kyZ"/>
|
||||||
|
<constraint firstAttribute="trailingMargin" secondItem="SFD-t6-Qk3" secondAttribute="trailing" id="g4i-l8-aln"/>
|
||||||
|
<constraint firstAttribute="bottomMargin" secondItem="SFD-t6-Qk3" secondAttribute="bottom" id="h38-79-xRT"/>
|
||||||
|
<constraint firstItem="SFD-t6-Qk3" firstAttribute="top" secondItem="YfI-8z-wCv" secondAttribute="topMargin" id="mCE-eA-UKd"/>
|
||||||
|
</constraints>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
<connections>
|
||||||
|
<outlet property="dateLabel" destination="0Md-jd-XXe" id="znM-W1-cZi"/>
|
||||||
|
<outlet property="errorDescriptionLabel" destination="Pwo-OM-Gm3" id="3zi-gX-I5t"/>
|
||||||
|
<outlet property="successLabel" destination="CZj-FM-9Qv" id="vtL-iu-aHR"/>
|
||||||
|
</connections>
|
||||||
|
</tableViewCell>
|
||||||
|
</prototypes>
|
||||||
|
<connections>
|
||||||
|
<outlet property="dataSource" destination="RUB-gO-oQ6" id="vqw-LK-wuY"/>
|
||||||
|
<outlet property="delegate" destination="RUB-gO-oQ6" id="9uY-h3-7qR"/>
|
||||||
|
</connections>
|
||||||
|
</tableView>
|
||||||
|
<navigationItem key="navigationItem" title="Refresh Attempts" largeTitleDisplayMode="never" id="jO4-kG-Alq">
|
||||||
|
<barButtonItem key="rightBarButtonItem" title="Sign Out" id="9Pj-GZ-Rra">
|
||||||
|
<color key="tintColor" red="1" green="0.14901960780000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="signOut:" destination="VBC-qD-V1a" id="m55-18-kgT"/>
|
||||||
|
</connections>
|
||||||
|
</barButtonItem>
|
||||||
|
</navigationItem>
|
||||||
|
</tableViewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="Wk8-xA-fxp" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="2313" y="515"/>
|
||||||
|
</scene>
|
||||||
<!--Browse-->
|
<!--Browse-->
|
||||||
<scene sceneID="VHa-uP-bFU">
|
<scene sceneID="VHa-uP-bFU">
|
||||||
<objects>
|
<objects>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ extension BackgroundTaskManager
|
|||||||
self.isPlaying = false
|
self.isPlaying = false
|
||||||
}
|
}
|
||||||
|
|
||||||
self.audioEngineQueue.async {
|
self.audioEngineQueue.sync {
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
|
try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
|
||||||
|
|||||||
@@ -56,6 +56,17 @@
|
|||||||
</uniquenessConstraint>
|
</uniquenessConstraint>
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="RefreshAttempt" representedClassName="RefreshAttempt" syncable="YES">
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="errorDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="isSuccess" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
@@ -84,7 +95,8 @@
|
|||||||
<element name="App" positionX="-63" positionY="-18" width="128" height="300"/>
|
<element name="App" positionX="-63" positionY="-18" width="128" height="300"/>
|
||||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
|
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
|
||||||
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
|
||||||
<element name="Source" positionX="-45" positionY="99" width="128" height="105"/>
|
<element name="Source" positionX="-45" positionY="99" width="128" height="105"/>
|
||||||
|
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||||
|
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
||||||
51
AltStore/Model/RefreshAttempt.swift
Normal file
51
AltStore/Model/RefreshAttempt.swift
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
//
|
||||||
|
// RefreshAttempt.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/31/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
@objc(RefreshAttempt)
|
||||||
|
class RefreshAttempt: NSManagedObject, Fetchable
|
||||||
|
{
|
||||||
|
@NSManaged var identifier: String
|
||||||
|
@NSManaged var date: Date
|
||||||
|
|
||||||
|
@NSManaged var isSuccess: Bool
|
||||||
|
@NSManaged var errorDescription: String?
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
init<T>(identifier: String, result: Result<T, Error>, context: NSManagedObjectContext)
|
||||||
|
{
|
||||||
|
super.init(entity: RefreshAttempt.entity(), insertInto: context)
|
||||||
|
|
||||||
|
self.identifier = identifier
|
||||||
|
self.date = Date()
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .success:
|
||||||
|
self.isSuccess = true
|
||||||
|
self.errorDescription = nil
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
self.isSuccess = false
|
||||||
|
self.errorDescription = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RefreshAttempt
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<RefreshAttempt>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<RefreshAttempt>(entityName: "RefreshAttempt")
|
||||||
|
}
|
||||||
|
}
|
||||||
73
AltStore/Settings/RefreshAttemptsViewController.swift
Normal file
73
AltStore/Settings/RefreshAttemptsViewController.swift
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
//
|
||||||
|
// RefreshAttemptsViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 7/31/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
@objc(RefreshAttemptTableViewCell)
|
||||||
|
private class RefreshAttemptTableViewCell: UITableViewCell
|
||||||
|
{
|
||||||
|
@IBOutlet var successLabel: UILabel!
|
||||||
|
@IBOutlet var dateLabel: UILabel!
|
||||||
|
@IBOutlet var errorDescriptionLabel: UILabel!
|
||||||
|
}
|
||||||
|
|
||||||
|
class RefreshAttemptsViewController: UITableViewController
|
||||||
|
{
|
||||||
|
private lazy var dataSource = self.makeDataSource()
|
||||||
|
|
||||||
|
private lazy var dateFormatter: DateFormatter = {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateStyle = .short
|
||||||
|
dateFormatter.timeStyle = .short
|
||||||
|
return dateFormatter
|
||||||
|
}()
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.tableView.dataSource = self.dataSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension RefreshAttemptsViewController
|
||||||
|
{
|
||||||
|
func makeDataSource() -> RSTFetchedResultsTableViewDataSource<RefreshAttempt>
|
||||||
|
{
|
||||||
|
let fetchRequest = RefreshAttempt.fetchRequest() as NSFetchRequest<RefreshAttempt>
|
||||||
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \RefreshAttempt.date, ascending: false)]
|
||||||
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
|
||||||
|
let dataSource = RSTFetchedResultsTableViewDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||||
|
dataSource.cellConfigurationHandler = { [weak self] (cell, attempt, indexPath) in
|
||||||
|
let cell = cell as! RefreshAttemptTableViewCell
|
||||||
|
cell.dateLabel.text = self?.dateFormatter.string(from: attempt.date)
|
||||||
|
cell.errorDescriptionLabel.text = attempt.errorDescription
|
||||||
|
|
||||||
|
if attempt.isSuccess
|
||||||
|
{
|
||||||
|
cell.successLabel.text = NSLocalizedString("Success", comment: "")
|
||||||
|
cell.successLabel.textColor = .altGreen
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.successLabel.text = NSLocalizedString("Failure", comment: "")
|
||||||
|
cell.successLabel.textColor = .refreshRed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let placeholderView = RSTPlaceholderView()
|
||||||
|
placeholderView.textLabel.text = NSLocalizedString("No Refresh Attempts", comment: "")
|
||||||
|
placeholderView.detailTextLabel.text = NSLocalizedString("The more you use AltStore, the more often iOS will allow it to refresh apps in the background.", comment: "")
|
||||||
|
dataSource.placeholderView = placeholderView
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user