mirror of
https://github.com/SideStore/SideStore.git
synced 2026-03-29 14:55:39 +02:00
Merge branch 'backup_apps' into develop
This commit is contained in:
10
AltBackup/AltBackup.entitlements
Normal file
10
AltBackup/AltBackup.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.rileytestut.AltStore</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
121
AltBackup/AppDelegate.swift
Normal file
121
AltBackup/AppDelegate.swift
Normal file
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/11/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension AppDelegate
|
||||
{
|
||||
static let startBackupNotification = Notification.Name("io.altstore.StartBackup")
|
||||
static let startRestoreNotification = Notification.Name("io.altstore.StartRestore")
|
||||
|
||||
static let operationDidFinishNotification = Notification.Name("io.altstore.BackupOperationFinished")
|
||||
|
||||
static let operationResultKey = "result"
|
||||
}
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
private var currentBackupReturnURL: URL?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
// Override point for customization after application launch.
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.operationDidFinish(_:)), name: AppDelegate.operationDidFinishNotification, object: nil)
|
||||
|
||||
let viewController = ViewController()
|
||||
|
||||
self.window = UIWindow(frame: UIScreen.main.bounds)
|
||||
self.window?.rootViewController = viewController
|
||||
self.window?.makeKeyAndVisible()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||
{
|
||||
return self.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate
|
||||
{
|
||||
func open(_ url: URL) -> Bool
|
||||
{
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||
guard let command = components.host?.lowercased() else { return false }
|
||||
|
||||
switch command
|
||||
{
|
||||
case "backup":
|
||||
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
||||
self.currentBackupReturnURL = returnURL
|
||||
NotificationCenter.default.post(name: AppDelegate.startBackupNotification, object: nil)
|
||||
|
||||
return true
|
||||
|
||||
case "restore":
|
||||
guard let returnString = components.queryItems?.first(where: { $0.name == "returnURL" })?.value, let returnURL = URL(string: returnString) else { return false }
|
||||
self.currentBackupReturnURL = returnURL
|
||||
NotificationCenter.default.post(name: AppDelegate.startRestoreNotification, object: nil)
|
||||
|
||||
return true
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
@objc func operationDidFinish(_ notification: Notification)
|
||||
{
|
||||
defer { self.currentBackupReturnURL = nil }
|
||||
|
||||
guard
|
||||
let returnURL = self.currentBackupReturnURL,
|
||||
let result = notification.userInfo?[AppDelegate.operationResultKey] as? Result<Void, Error>
|
||||
else { return }
|
||||
|
||||
guard var components = URLComponents(url: returnURL, resolvingAgainstBaseURL: false) else { return }
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
components.path = "/success"
|
||||
|
||||
case .failure(let error as NSError):
|
||||
components.path = "/failure"
|
||||
components.queryItems = ["errorDomain": error.domain,
|
||||
"errorCode": String(error.code),
|
||||
"errorDescription": error.localizedDescription].map { URLQueryItem(name: $0, value: $1) }
|
||||
}
|
||||
|
||||
guard let responseURL = components.url else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(responseURL, options: [:]) { (success) in
|
||||
print("Sent response to app with success:", success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
98
AltBackup/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
98
AltBackup/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
38
AltBackup/Assets.xcassets/Background.colorset/Contents.json
Normal file
38
AltBackup/Assets.xcassets/Background.colorset/Contents.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.518",
|
||||
"green" : "0.502",
|
||||
"red" : "0.004"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.404",
|
||||
"green" : "0.322",
|
||||
"red" : "0.008"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
AltBackup/Assets.xcassets/Contents.json
Normal file
6
AltBackup/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
20
AltBackup/Assets.xcassets/Text.colorset/Contents.json
Normal file
20
AltBackup/Assets.xcassets/Text.colorset/Contents.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.750",
|
||||
"blue" : "0xFF",
|
||||
"green" : "0xFF",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
293
AltBackup/BackupController.swift
Normal file
293
AltBackup/BackupController.swift
Normal file
@@ -0,0 +1,293 @@
|
||||
//
|
||||
// BackupController.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/12/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ErrorUserInfoKey
|
||||
{
|
||||
static let sourceFile: String = "alt_sourceFile"
|
||||
static let sourceFileLine: String = "alt_sourceFileLine"
|
||||
}
|
||||
|
||||
extension Error
|
||||
{
|
||||
var sourceDescription: String? {
|
||||
guard let sourceFile = (self as NSError).userInfo[ErrorUserInfoKey.sourceFile] as? String, let sourceFileLine = (self as NSError).userInfo[ErrorUserInfoKey.sourceFileLine] else {
|
||||
return nil
|
||||
}
|
||||
return "(\((sourceFile as NSString).lastPathComponent), Line \(sourceFileLine))"
|
||||
}
|
||||
}
|
||||
|
||||
struct BackupError: ALTLocalizedError
|
||||
{
|
||||
enum Code
|
||||
{
|
||||
case invalidBundleID
|
||||
case appGroupNotFound(String?)
|
||||
case randomError // Used for debugging.
|
||||
}
|
||||
|
||||
let code: Code
|
||||
|
||||
let sourceFile: String
|
||||
let sourceFileLine: Int
|
||||
|
||||
var errorFailure: String?
|
||||
|
||||
var failureReason: String? {
|
||||
switch self.code
|
||||
{
|
||||
case .invalidBundleID: return NSLocalizedString("The bundle identifier is invalid.", comment: "")
|
||||
case .appGroupNotFound(let appGroup):
|
||||
if let appGroup = appGroup
|
||||
{
|
||||
return String(format: NSLocalizedString("The app group “%@” could not be found.", comment: ""), appGroup)
|
||||
}
|
||||
else
|
||||
{
|
||||
return NSLocalizedString("The AltStore app group could not be found.", comment: "")
|
||||
}
|
||||
case .randomError: return NSLocalizedString("A random error occured.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var errorUserInfo: [String : Any] {
|
||||
let userInfo: [String: Any?] = [NSLocalizedDescriptionKey: self.errorDescription,
|
||||
NSLocalizedFailureReasonErrorKey: self.failureReason,
|
||||
NSLocalizedFailureErrorKey: self.errorFailure,
|
||||
ErrorUserInfoKey.sourceFile: self.sourceFile,
|
||||
ErrorUserInfoKey.sourceFileLine: self.sourceFileLine]
|
||||
return userInfo.compactMapValues { $0 }
|
||||
}
|
||||
|
||||
init(_ code: Code, description: String? = nil, file: String = #file, line: Int = #line)
|
||||
{
|
||||
self.code = code
|
||||
self.errorFailure = description
|
||||
self.sourceFile = file
|
||||
self.sourceFileLine = line
|
||||
}
|
||||
}
|
||||
|
||||
class BackupController: NSObject
|
||||
{
|
||||
private let fileCoordinator = NSFileCoordinator(filePresenter: nil)
|
||||
private let operationQueue = OperationQueue()
|
||||
|
||||
override init()
|
||||
{
|
||||
self.operationQueue.name = "AltBackup-BackupQueue"
|
||||
}
|
||||
|
||||
func performBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
||||
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to create backup directory.", comment: ""))
|
||||
}
|
||||
|
||||
guard
|
||||
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
||||
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
||||
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to create backup directory.", comment: "")) }
|
||||
|
||||
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
||||
|
||||
// Use temporary directory to prevent messing up successful backup with incomplete one.
|
||||
let temporaryAppBackupDirectory = backupsDirectory.appendingPathComponent("Temp", isDirectory: true).appendingPathComponent(UUID().uuidString)
|
||||
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
|
||||
|
||||
let writingIntent = NSFileAccessIntent.writingIntent(with: temporaryAppBackupDirectory, options: [])
|
||||
let replacementIntent = NSFileAccessIntent.writingIntent(with: appBackupDirectory, options: [.forReplacing])
|
||||
self.fileCoordinator.coordinate(with: [writingIntent, replacementIntent], queue: self.operationQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
let mainGroupBackupDirectory = temporaryAppBackupDirectory.appendingPathComponent("App")
|
||||
try FileManager.default.createDirectory(at: mainGroupBackupDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: backupDocumentsDirectory.path)
|
||||
{
|
||||
try FileManager.default.removeItem(at: backupDocumentsDirectory)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: documentsDirectory.path)
|
||||
{
|
||||
try FileManager.default.copyItem(at: documentsDirectory, to: backupDocumentsDirectory)
|
||||
}
|
||||
|
||||
print("Copied Documents directory from \(documentsDirectory) to \(backupDocumentsDirectory)")
|
||||
|
||||
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
|
||||
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: backupLibraryDirectory.path)
|
||||
{
|
||||
try FileManager.default.removeItem(at: backupLibraryDirectory)
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: libraryDirectory.path)
|
||||
{
|
||||
try FileManager.default.copyItem(at: libraryDirectory, to: backupLibraryDirectory)
|
||||
}
|
||||
|
||||
print("Copied Library directory from \(libraryDirectory) to \(backupLibraryDirectory)")
|
||||
}
|
||||
|
||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
|
||||
{
|
||||
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to create app group backup directory.", comment: ""))
|
||||
}
|
||||
|
||||
let backupAppGroupURL = temporaryAppBackupDirectory.appendingPathComponent(appGroup)
|
||||
|
||||
// There are several system hidden files that we don't have permission to read, so we just skip all hidden files in app group directories.
|
||||
try self.copyDirectoryContents(at: appGroupURL, to: backupAppGroupURL, options: [.skipsHiddenFiles])
|
||||
}
|
||||
|
||||
// Replace previous backup with new backup.
|
||||
_ = try FileManager.default.replaceItemAt(appBackupDirectory, withItemAt: temporaryAppBackupDirectory)
|
||||
|
||||
print("Replaced previous backup with new backup:", temporaryAppBackupDirectory)
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
do { try FileManager.default.removeItem(at: temporaryAppBackupDirectory) }
|
||||
catch { print("Failed to remove temporary directory.", error) }
|
||||
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
func restoreBackup(completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let bundleIdentifier = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.altBundleID) as? String else {
|
||||
throw BackupError(.invalidBundleID, description: NSLocalizedString("Unable to access backup.", comment: ""))
|
||||
}
|
||||
|
||||
guard
|
||||
let altstoreAppGroup = Bundle.main.altstoreAppGroup,
|
||||
let sharedDirectoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: altstoreAppGroup)
|
||||
else { throw BackupError(.appGroupNotFound(nil), description: NSLocalizedString("Unable to access backup.", comment: "")) }
|
||||
|
||||
let backupsDirectory = sharedDirectoryURL.appendingPathComponent("Backups")
|
||||
let appBackupDirectory = backupsDirectory.appendingPathComponent(bundleIdentifier)
|
||||
|
||||
let readingIntent = NSFileAccessIntent.readingIntent(with: appBackupDirectory, options: [])
|
||||
self.fileCoordinator.coordinate(with: [readingIntent], queue: self.operationQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
let mainGroupBackupDirectory = appBackupDirectory.appendingPathComponent("App")
|
||||
|
||||
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let backupDocumentsDirectory = mainGroupBackupDirectory.appendingPathComponent(documentsDirectory.lastPathComponent)
|
||||
|
||||
let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
|
||||
let backupLibraryDirectory = mainGroupBackupDirectory.appendingPathComponent(libraryDirectory.lastPathComponent)
|
||||
|
||||
try self.copyDirectoryContents(at: backupDocumentsDirectory, to: documentsDirectory)
|
||||
try self.copyDirectoryContents(at: backupLibraryDirectory, to: libraryDirectory)
|
||||
|
||||
for appGroup in Bundle.main.appGroups where appGroup != altstoreAppGroup
|
||||
{
|
||||
guard let appGroupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
throw BackupError(.appGroupNotFound(appGroup), description: NSLocalizedString("Unable to read app group backup.", comment: ""))
|
||||
}
|
||||
|
||||
let backupAppGroupURL = appBackupDirectory.appendingPathComponent(appGroup)
|
||||
try self.copyDirectoryContents(at: backupAppGroupURL, to: appGroupURL)
|
||||
}
|
||||
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension BackupController
|
||||
{
|
||||
func copyDirectoryContents(at sourceDirectoryURL: URL, to destinationDirectoryURL: URL, options: FileManager.DirectoryEnumerationOptions = []) throws
|
||||
{
|
||||
guard FileManager.default.fileExists(atPath: sourceDirectoryURL.path) else { return }
|
||||
|
||||
if !FileManager.default.fileExists(atPath: destinationDirectoryURL.path)
|
||||
{
|
||||
try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
for fileURL in try FileManager.default.contentsOfDirectory(at: sourceDirectoryURL, includingPropertiesForKeys: [.isDirectoryKey], options: options)
|
||||
{
|
||||
let isDirectory = try fileURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false
|
||||
let destinationURL = destinationDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
|
||||
|
||||
if FileManager.default.fileExists(atPath: destinationURL.path)
|
||||
{
|
||||
do {
|
||||
try FileManager.default.removeItem(at: destinationURL)
|
||||
}
|
||||
catch CocoaError.fileWriteNoPermission where isDirectory {
|
||||
try self.copyDirectoryContents(at: fileURL, to: destinationURL, options: options)
|
||||
continue
|
||||
}
|
||||
catch {
|
||||
print(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
||||
print("Copied item from \(fileURL) to \(destinationURL)")
|
||||
}
|
||||
catch let error where fileURL.lastPathComponent == "Inbox" && fileURL.deletingLastPathComponent().lastPathComponent == "Documents" {
|
||||
// Ignore errors for /Documents/Inbox
|
||||
print("Failed to copy Inbox directory:", error)
|
||||
}
|
||||
catch {
|
||||
print(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
AltBackup/Base.lproj/LaunchScreen.storyboard
Normal file
32
AltBackup/Base.lproj/LaunchScreen.storyboard
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" name="Background"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<namedColor name="Background">
|
||||
<color red="0.0040000001899898052" green="0.50199997425079346" blue="0.51800000667572021" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
||||
66
AltBackup/Info.plist
Normal file
66
AltBackup/Info.plist
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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>ALTAppGroups</key>
|
||||
<array>
|
||||
<string>group.com.rileytestut.AltStore</string>
|
||||
</array>
|
||||
<key>ALTBundleIdentifier</key>
|
||||
<string>com.rileytestut.AltBackup</string>
|
||||
<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>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>AltBackup General</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>altbackup</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
15
AltBackup/UIColor+AltBackup.swift
Normal file
15
AltBackup/UIColor+AltBackup.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// UIColor+AltBackup.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/11/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIColor
|
||||
{
|
||||
static let altstoreBackground = UIColor(named: "Background")!
|
||||
static let altstoreText = UIColor(named: "Text")!
|
||||
}
|
||||
206
AltBackup/ViewController.swift
Normal file
206
AltBackup/ViewController.swift
Normal file
@@ -0,0 +1,206 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// AltBackup
|
||||
//
|
||||
// Created by Riley Testut on 5/11/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension Bundle
|
||||
{
|
||||
var appName: String? {
|
||||
let appName =
|
||||
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ??
|
||||
Bundle.main.object(forInfoDictionaryKey: kCFBundleNameKey as String) as? String
|
||||
return appName
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewController
|
||||
{
|
||||
enum BackupOperation
|
||||
{
|
||||
case backup
|
||||
case restore
|
||||
}
|
||||
}
|
||||
|
||||
class ViewController: UIViewController
|
||||
{
|
||||
private let backupController = BackupController()
|
||||
|
||||
private var currentOperation: BackupOperation? {
|
||||
didSet {
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var textLabel: UILabel!
|
||||
private var detailTextLabel: UILabel!
|
||||
private var activityIndicatorView: UIActivityIndicatorView!
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
|
||||
{
|
||||
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.backup), name: AppDelegate.startBackupNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.restore), name: AppDelegate.startRestoreNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.backgroundColor = .altstoreBackground
|
||||
|
||||
self.textLabel = UILabel(frame: .zero)
|
||||
self.textLabel.font = UIFont.preferredFont(forTextStyle: .title2)
|
||||
self.textLabel.textColor = .altstoreText
|
||||
self.textLabel.textAlignment = .center
|
||||
self.textLabel.numberOfLines = 0
|
||||
|
||||
self.detailTextLabel = UILabel(frame: .zero)
|
||||
self.detailTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
self.detailTextLabel.textColor = .altstoreText
|
||||
self.detailTextLabel.textAlignment = .center
|
||||
self.detailTextLabel.numberOfLines = 0
|
||||
|
||||
self.activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge)
|
||||
self.activityIndicatorView.color = .altstoreText
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
#if DEBUG
|
||||
let button1 = UIButton(type: .system)
|
||||
button1.setTitle("Backup", for: .normal)
|
||||
button1.setTitleColor(.white, for: .normal)
|
||||
button1.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button1.addTarget(self, action: #selector(ViewController.backup), for: .primaryActionTriggered)
|
||||
|
||||
let button2 = UIButton(type: .system)
|
||||
button2.setTitle("Restore", for: .normal)
|
||||
button2.setTitleColor(.white, for: .normal)
|
||||
button2.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
button2.addTarget(self, action: #selector(ViewController.restore), for: .primaryActionTriggered)
|
||||
|
||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!, button1, button2]
|
||||
#else
|
||||
let arrangedSubviews = [self.textLabel!, self.detailTextLabel!, self.activityIndicatorView!]
|
||||
#endif
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.spacing = 22
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
self.view.addSubview(stackView)
|
||||
|
||||
NSLayoutConstraint.activate([stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
|
||||
stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
|
||||
stackView.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: self.view.safeAreaLayoutGuide.leadingAnchor, multiplier: 1.0),
|
||||
self.view.safeAreaLayoutGuide.trailingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: stackView.trailingAnchor, multiplier: 1.0)])
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
private extension ViewController
|
||||
{
|
||||
@objc func backup()
|
||||
{
|
||||
self.currentOperation = .backup
|
||||
|
||||
self.backupController.performBackup { (result) in
|
||||
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
|
||||
|
||||
let title = String(format: NSLocalizedString("%@ could not be backed up.", comment: ""), appName)
|
||||
self.process(result, errorTitle: title)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func restore()
|
||||
{
|
||||
self.currentOperation = .restore
|
||||
|
||||
self.backupController.restoreBackup { (result) in
|
||||
let appName = Bundle.main.appName ?? NSLocalizedString("App", comment: "")
|
||||
|
||||
let title = String(format: NSLocalizedString("%@ could not be restored.", comment: ""), appName)
|
||||
self.process(result, errorTitle: title)
|
||||
}
|
||||
}
|
||||
|
||||
func update()
|
||||
{
|
||||
switch self.currentOperation
|
||||
{
|
||||
case .backup:
|
||||
self.textLabel.text = NSLocalizedString("Backing up app data…", comment: "")
|
||||
self.detailTextLabel.isHidden = true
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
case .restore:
|
||||
self.textLabel.text = NSLocalizedString("Restoring app data…", comment: "")
|
||||
self.detailTextLabel.isHidden = true
|
||||
self.activityIndicatorView.startAnimating()
|
||||
|
||||
case .none:
|
||||
self.textLabel.text = String(format: NSLocalizedString("%@ is inactive.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("App", comment: ""))
|
||||
|
||||
self.detailTextLabel.text = String(format: NSLocalizedString("Refresh %@ in AltStore to continue using it.", comment: ""),
|
||||
Bundle.main.appName ?? NSLocalizedString("this app", comment: ""))
|
||||
|
||||
self.detailTextLabel.isHidden = false
|
||||
self.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ViewController
|
||||
{
|
||||
func process(_ result: Result<Void, Error>, errorTitle: String)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error as NSError):
|
||||
let message: String
|
||||
|
||||
if let sourceDescription = error.sourceDescription
|
||||
{
|
||||
message = error.localizedDescription + "\n\n" + sourceDescription
|
||||
}
|
||||
else
|
||||
{
|
||||
message = error.localizedDescription
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: errorTitle, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: AppDelegate.operationDidFinishNotification, object: nil, userInfo: [AppDelegate.operationResultKey: result])
|
||||
}
|
||||
}
|
||||
|
||||
@objc func didEnterBackground(_ notification: Notification)
|
||||
{
|
||||
// Reset UI once we've left app (but not before).
|
||||
self.currentOperation = nil
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public extension Bundle
|
||||
public static let serverID = "ALTServerID"
|
||||
public static let certificateID = "ALTCertificateID"
|
||||
public static let appGroups = "ALTAppGroups"
|
||||
public static let altBundleID = "ALTBundleIdentifier"
|
||||
|
||||
public static let urlTypes = "CFBundleURLTypes"
|
||||
public static let exportedUTIs = "UTExportedTypeDeclarations"
|
||||
@@ -24,6 +25,8 @@ public extension Bundle
|
||||
|
||||
public extension Bundle
|
||||
{
|
||||
static var baseAltStoreAppGroupID = "group.com.rileytestut.AltStore"
|
||||
|
||||
var infoPlistURL: URL {
|
||||
let infoPlistURL = self.bundleURL.appendingPathComponent("Info.plist")
|
||||
return infoPlistURL
|
||||
@@ -38,4 +41,13 @@ public extension Bundle
|
||||
let infoPlistURL = self.bundleURL.appendingPathComponent("ALTCertificate.p12")
|
||||
return infoPlistURL
|
||||
}
|
||||
|
||||
var appGroups: [String] {
|
||||
return self.infoDictionary?[Bundle.Info.appGroups] as? [String] ?? []
|
||||
}
|
||||
|
||||
var altstoreAppGroup: String? {
|
||||
let appGroup = self.appGroups.first { $0.contains(Bundle.baseAltStoreAppGroupID) }
|
||||
return appGroup
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,13 @@ struct CodableServerError: Codable
|
||||
{
|
||||
self.errorCode = error.code
|
||||
|
||||
let userInfo = error.userInfo.compactMapValues { $0 as? String }
|
||||
var userInfo = error.userInfo.compactMapValues { $0 as? String }
|
||||
|
||||
if let localizedRecoverySuggestion = (error as NSError).localizedRecoverySuggestion
|
||||
{
|
||||
userInfo[NSLocalizedRecoverySuggestionErrorKey] = localizedRecoverySuggestion
|
||||
}
|
||||
|
||||
if !userInfo.isEmpty
|
||||
{
|
||||
self.userInfo = userInfo
|
||||
|
||||
@@ -39,7 +39,9 @@ typedef NS_ERROR_ENUM(AltServerErrorDomain, ALTServerError)
|
||||
ALTServerErrorInvalidAnisetteData = 13,
|
||||
ALTServerErrorPluginNotFound = 14,
|
||||
|
||||
ALTServerErrorProfileNotFound = 15
|
||||
ALTServerErrorProfileNotFound = 15,
|
||||
|
||||
ALTServerErrorAppDeletionFailed = 16,
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -52,7 +52,11 @@ NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdenti
|
||||
return NSLocalizedString(@"An unknown error occured.", @"");
|
||||
|
||||
case ALTServerErrorConnectionFailed:
|
||||
#if TARGET_OS_OSX
|
||||
return NSLocalizedString(@"Could not connect to device.", @"");
|
||||
#else
|
||||
return NSLocalizedString(@"Could not connect to AltServer.", @"");
|
||||
#endif
|
||||
|
||||
case ALTServerErrorLostConnection:
|
||||
return NSLocalizedString(@"Lost connection to AltServer.", @"");
|
||||
@@ -95,6 +99,9 @@ NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdenti
|
||||
|
||||
case ALTServerErrorProfileNotFound:
|
||||
return [self profileErrorLocalizedDescriptionWithBaseDescription:NSLocalizedString(@"Could not find profile", "")];
|
||||
|
||||
case ALTServerErrorAppDeletionFailed:
|
||||
return NSLocalizedString(@"An error occured while removing the app.", @"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,11 +111,14 @@ NSErrorUserInfoKey const ALTProvisioningProfileBundleIDErrorKey = @"bundleIdenti
|
||||
{
|
||||
case ALTServerErrorConnectionFailed:
|
||||
case ALTServerErrorDeviceNotFound:
|
||||
return NSLocalizedString(@"Make sure you have trusted this phone with your computer and WiFi sync is enabled.", @"");
|
||||
return NSLocalizedString(@"Make sure you have trusted this device with your computer and WiFi sync is enabled.", @"");
|
||||
|
||||
case ALTServerErrorPluginNotFound:
|
||||
return NSLocalizedString(@"Make sure Mail is running and the plug-in is enabled in Mail's preferences.", @"");
|
||||
|
||||
case ALTServerErrorMaximumFreeAppLimitReached:
|
||||
return NSLocalizedString(@"Make sure “Offload Unused Apps” is disabled in Settings > iTunes & App Stores, then install or delete all offloaded apps.", @"");
|
||||
|
||||
default:
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public enum ServerRequest: Decodable
|
||||
case beginInstallation(BeginInstallationRequest)
|
||||
case installProvisioningProfiles(InstallProvisioningProfilesRequest)
|
||||
case removeProvisioningProfiles(RemoveProvisioningProfilesRequest)
|
||||
case removeApp(RemoveAppRequest)
|
||||
case unknown(identifier: String, version: Int)
|
||||
|
||||
var identifier: String {
|
||||
@@ -34,6 +35,7 @@ public enum ServerRequest: Decodable
|
||||
case .beginInstallation(let request): return request.identifier
|
||||
case .installProvisioningProfiles(let request): return request.identifier
|
||||
case .removeProvisioningProfiles(let request): return request.identifier
|
||||
case .removeApp(let request): return request.identifier
|
||||
case .unknown(let identifier, _): return identifier
|
||||
}
|
||||
}
|
||||
@@ -46,6 +48,7 @@ public enum ServerRequest: Decodable
|
||||
case .beginInstallation(let request): return request.version
|
||||
case .installProvisioningProfiles(let request): return request.version
|
||||
case .removeProvisioningProfiles(let request): return request.version
|
||||
case .removeApp(let request): return request.version
|
||||
case .unknown(_, let version): return version
|
||||
}
|
||||
}
|
||||
@@ -85,6 +88,10 @@ public enum ServerRequest: Decodable
|
||||
let request = try RemoveProvisioningProfilesRequest(from: decoder)
|
||||
self = .removeProvisioningProfiles(request)
|
||||
|
||||
case "RemoveAppRequest":
|
||||
let request = try RemoveAppRequest(from: decoder)
|
||||
self = .removeApp(request)
|
||||
|
||||
default:
|
||||
self = .unknown(identifier: identifier, version: version)
|
||||
}
|
||||
@@ -97,6 +104,7 @@ public enum ServerResponse: Decodable
|
||||
case installationProgress(InstallationProgressResponse)
|
||||
case installProvisioningProfiles(InstallProvisioningProfilesResponse)
|
||||
case removeProvisioningProfiles(RemoveProvisioningProfilesResponse)
|
||||
case removeApp(RemoveAppResponse)
|
||||
case error(ErrorResponse)
|
||||
case unknown(identifier: String, version: Int)
|
||||
|
||||
@@ -107,6 +115,7 @@ public enum ServerResponse: Decodable
|
||||
case .installationProgress(let response): return response.identifier
|
||||
case .installProvisioningProfiles(let response): return response.identifier
|
||||
case .removeProvisioningProfiles(let response): return response.identifier
|
||||
case .removeApp(let response): return response.identifier
|
||||
case .error(let response): return response.identifier
|
||||
case .unknown(let identifier, _): return identifier
|
||||
}
|
||||
@@ -119,6 +128,7 @@ public enum ServerResponse: Decodable
|
||||
case .installationProgress(let response): return response.version
|
||||
case .installProvisioningProfiles(let response): return response.version
|
||||
case .removeProvisioningProfiles(let response): return response.version
|
||||
case .removeApp(let response): return response.version
|
||||
case .error(let response): return response.version
|
||||
case .unknown(_, let version): return version
|
||||
}
|
||||
@@ -155,6 +165,10 @@ public enum ServerResponse: Decodable
|
||||
let response = try RemoveProvisioningProfilesResponse(from: decoder)
|
||||
self = .removeProvisioningProfiles(response)
|
||||
|
||||
case "RemoveAppResponse":
|
||||
let response = try RemoveAppResponse(from: decoder)
|
||||
self = .removeApp(response)
|
||||
|
||||
case "ErrorResponse":
|
||||
let response = try ErrorResponse(from: decoder)
|
||||
self = .error(response)
|
||||
@@ -379,3 +393,28 @@ public struct RemoveProvisioningProfilesResponse: ServerMessageProtocol
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public struct RemoveAppRequest: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "RemoveAppRequest"
|
||||
|
||||
public var udid: String
|
||||
public var bundleIdentifier: String
|
||||
|
||||
public init(udid: String, bundleIdentifier: String)
|
||||
{
|
||||
self.udid = udid
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
}
|
||||
}
|
||||
|
||||
public struct RemoveAppResponse: ServerMessageProtocol
|
||||
{
|
||||
public var version = 1
|
||||
public var identifier = "RemoveAppResponse"
|
||||
|
||||
public init()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +212,10 @@ private extension AppDelegate
|
||||
{
|
||||
alert.informativeText = underlyingError.localizedDescription
|
||||
}
|
||||
else if let recoverySuggestion = error.localizedRecoverySuggestion
|
||||
{
|
||||
alert.informativeText = error.localizedDescription + "\n\n" + recoverySuggestion
|
||||
}
|
||||
else
|
||||
{
|
||||
alert.informativeText = error.localizedDescription
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
uint32_t size = MIN(4096, (uint32_t)expectedSize - (uint32_t)receivedData.length);
|
||||
|
||||
uint32_t receivedBytes = 0;
|
||||
if (idevice_connection_receive_timeout(self.connection, bytes, size, &receivedBytes, 0) != IDEVICE_E_SUCCESS)
|
||||
if (idevice_connection_receive_timeout(self.connection, bytes, size, &receivedBytes, 10000) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish(nil, [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorLostConnection userInfo:nil]);
|
||||
}
|
||||
|
||||
@@ -273,6 +273,9 @@ private extension ConnectionManager
|
||||
case .success(.removeProvisioningProfiles(let request)):
|
||||
self.handleRemoveProvisioningProfilesRequest(request, for: connection)
|
||||
|
||||
case .success(.removeApp(let request)):
|
||||
self.handleRemoveAppRequest(request, for: connection)
|
||||
|
||||
case .success(.unknown):
|
||||
let response = ErrorResponse(error: ALTServerError(.unknownRequest))
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
@@ -485,7 +488,31 @@ private extension ConnectionManager
|
||||
|
||||
let response = RemoveProvisioningProfilesResponse()
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent remove profiles error response to \(connection) with result:", result)
|
||||
print("Sent remove profiles success response to \(connection) with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveAppRequest(_ request: RemoveAppRequest, for connection: ClientConnection)
|
||||
{
|
||||
ALTDeviceManager.shared.removeApp(forBundleIdentifier: request.bundleIdentifier, fromDeviceWithUDID: request.udid) { (success, error) in
|
||||
if let error = error, !success
|
||||
{
|
||||
print("Failed to remove app \(request.bundleIdentifier):", error)
|
||||
|
||||
let errorResponse = ErrorResponse(error: ALTServerError(error))
|
||||
connection.send(errorResponse, shouldDisconnect: true) { (result) in
|
||||
print("Sent remove a[[ error response with result:", result)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Removed app:", request.bundleIdentifier)
|
||||
|
||||
let response = RemoveAppResponse()
|
||||
connection.send(response, shouldDisconnect: true) { (result) in
|
||||
print("Sent remove app success response to \(connection) with result:", result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore-sta
|
||||
private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")!
|
||||
#endif
|
||||
|
||||
private let appGroupsLock = NSLock()
|
||||
|
||||
enum InstallError: LocalizedError
|
||||
{
|
||||
case cancelled
|
||||
@@ -125,18 +127,29 @@ extension ALTDeviceManager
|
||||
{
|
||||
let appID = try result.get()
|
||||
|
||||
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
|
||||
self.updateAppGroups(for: appID, app: application, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let provisioningProfile = try result.get()
|
||||
let appID = try result.get()
|
||||
|
||||
self.install(application, to: device, team: team, appID: appID, certificate: certificate, profile: provisioningProfile) { (result) in
|
||||
finish(result.error, title: "Failed to Install AltStore")
|
||||
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
|
||||
do
|
||||
{
|
||||
let provisioningProfile = try result.get()
|
||||
|
||||
self.install(application, to: device, team: team, appID: appID, certificate: certificate, profile: provisioningProfile) { (result) in
|
||||
finish(result.error, title: "Failed to Install AltStore")
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Fetch Provisioning Profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(error, title: "Failed to Fetch Provisioning Profile")
|
||||
finish(error, title: "Failed to Update App Groups")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,11 +481,119 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
features[.appGroups] = true
|
||||
}
|
||||
|
||||
let appID = appID.copy() as! ALTAppID
|
||||
appID.features = features
|
||||
var updateFeatures = false
|
||||
|
||||
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
// Determine whether the required features are already enabled for the AppID.
|
||||
for (feature, value) in features
|
||||
{
|
||||
if let appIDValue = appID.features[feature] as AnyObject?, (value as AnyObject).isEqual(appIDValue)
|
||||
{
|
||||
// AppID already has this feature enabled and the values are the same.
|
||||
continue
|
||||
}
|
||||
else
|
||||
{
|
||||
// AppID either doesn't have this feature enabled or the value has changed,
|
||||
// so we need to update it to reflect new values.
|
||||
updateFeatures = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if updateFeatures
|
||||
{
|
||||
let appID = appID.copy() as! ALTAppID
|
||||
appID.features = features
|
||||
|
||||
ALTAppleAPI.shared.update(appID, team: team, session: session) { (appID, error) in
|
||||
completionHandler(Result(appID, error))
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(appID))
|
||||
}
|
||||
}
|
||||
|
||||
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let applicationGroups = app.entitlements[.appGroups] as? [String] ?? []
|
||||
if applicationGroups.isEmpty
|
||||
{
|
||||
guard let isAppGroupsEnabled = appID.features[.appGroups] as? Bool, isAppGroupsEnabled else {
|
||||
// No app groups, and we also haven't enabled the feature, so don't continue.
|
||||
// For apps with no app groups but have had the feature enabled already
|
||||
// we'll continue and assign the app ID to an empty array
|
||||
// in case we need to explicitly remove them.
|
||||
return completionHandler(.success(appID))
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch onto global queue to prevent appGroupsLock deadlock.
|
||||
DispatchQueue.global().async {
|
||||
|
||||
// Ensure we're not concurrently fetching and updating app groups,
|
||||
// which can lead to race conditions such as adding an app group twice.
|
||||
appGroupsLock.lock()
|
||||
|
||||
func finish(_ result: Result<ALTAppID, Error>)
|
||||
{
|
||||
appGroupsLock.unlock()
|
||||
completionHandler(result)
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
|
||||
switch Result(groups, error)
|
||||
{
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let fetchedGroups):
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
var groups = [ALTAppGroup]()
|
||||
var errors = [Error]()
|
||||
|
||||
for groupIdentifier in applicationGroups
|
||||
{
|
||||
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
|
||||
|
||||
if let group = fetchedGroups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
|
||||
{
|
||||
groups.append(group)
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
|
||||
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
|
||||
|
||||
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
|
||||
switch Result(group, error)
|
||||
{
|
||||
case .success(let group): groups.append(group)
|
||||
case .failure(let error): errors.append(error)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
if let error = errors.first
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.assign(appID, to: Array(groups), team: team, session: session) { (success, error) in
|
||||
let result = Result(success, error)
|
||||
finish(result.map { _ in appID })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,6 +641,24 @@ To prevent this from happening, feel free to try again with another Apple ID to
|
||||
infoDictionary[Bundle.Info.deviceID] = device.identifier
|
||||
infoDictionary[Bundle.Info.serverID] = UserDefaults.standard.serverID
|
||||
infoDictionary[Bundle.Info.certificateID] = certificate.serialNumber
|
||||
|
||||
let openAppURL = URL(string: "altstore-" + application.bundleIdentifier + "://")!
|
||||
|
||||
var allURLSchemes = infoDictionary[Bundle.Info.urlTypes] as? [[String: Any]] ?? []
|
||||
|
||||
// Embed open URL so AltBackup can return to AltStore.
|
||||
let altstoreURLScheme = ["CFBundleTypeRole": "Editor",
|
||||
"CFBundleURLName": application.bundleIdentifier,
|
||||
"CFBundleURLSchemes": [openAppURL.scheme!]] as [String : Any]
|
||||
allURLSchemes.append(altstoreURLScheme)
|
||||
|
||||
infoDictionary[Bundle.Info.urlTypes] = allURLSchemes
|
||||
|
||||
if let appGroups = profile.entitlements[.appGroups] as? [String]
|
||||
{
|
||||
infoDictionary[Bundle.Info.appGroups] = appGroups
|
||||
}
|
||||
|
||||
try (infoDictionary as NSDictionary).write(to: infoPlistURL)
|
||||
|
||||
if
|
||||
|
||||
@@ -28,6 +28,7 @@ extern NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification
|
||||
|
||||
/* App Installation */
|
||||
- (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
- (void)removeAppForBundleIdentifier:(NSString *)bundleIdentifier fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
- (void)installProvisioningProfiles:(NSSet<ALTProvisioningProfile *> *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
- (void)removeProvisioningProfilesForBundleIdentifiers:(NSSet<NSString *> *)bundleIdentifiers fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <libimobiledevice/misagent.h>
|
||||
|
||||
void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *udid);
|
||||
void ALTDeviceManagerUpdateAppDeletionStatus(plist_t command, plist_t status, void *uuid);
|
||||
void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data);
|
||||
|
||||
NSNotificationName const ALTDeviceManagerDeviceDidConnectNotification = @"ALTDeviceManagerDeviceDidConnectNotification";
|
||||
@@ -28,6 +29,8 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
|
||||
@interface ALTDeviceManager ()
|
||||
|
||||
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, void (^)(NSError *)> *installationCompletionHandlers;
|
||||
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, void (^)(NSError *)> *deletionCompletionHandlers;
|
||||
|
||||
@property (nonatomic, readonly) NSMutableDictionary<NSUUID *, NSProgress *> *installationProgress;
|
||||
@property (nonatomic, readonly) dispatch_queue_t installationQueue;
|
||||
|
||||
@@ -54,8 +57,9 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
|
||||
if (self)
|
||||
{
|
||||
_installationCompletionHandlers = [NSMutableDictionary dictionary];
|
||||
_installationProgress = [NSMutableDictionary dictionary];
|
||||
_deletionCompletionHandlers = [NSMutableDictionary dictionary];
|
||||
|
||||
_installationProgress = [NSMutableDictionary dictionary];
|
||||
_installationQueue = dispatch_queue_create("com.rileytestut.AltServer.InstallationQueue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_cachedDevices = [NSMutableSet set];
|
||||
@@ -498,6 +502,87 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
|
||||
return success;
|
||||
}
|
||||
|
||||
- (void)removeAppForBundleIdentifier:(NSString *)bundleIdentifier fromDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler
|
||||
{
|
||||
__block idevice_t device = NULL;
|
||||
__block lockdownd_client_t client = NULL;
|
||||
__block instproxy_client_t ipc = NULL;
|
||||
__block lockdownd_service_descriptor_t service = NULL;
|
||||
|
||||
void (^finish)(NSError *error) = ^(NSError *e) {
|
||||
__block NSError *error = e;
|
||||
|
||||
lockdownd_service_descriptor_free(service);
|
||||
instproxy_client_free(ipc);
|
||||
lockdownd_client_free(client);
|
||||
idevice_free(device);
|
||||
|
||||
if (error != nil)
|
||||
{
|
||||
completionHandler(NO, error);
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(YES, nil);
|
||||
}
|
||||
};
|
||||
|
||||
/* Find Device */
|
||||
if (idevice_new(&device, udid.UTF8String) != IDEVICE_E_SUCCESS)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorDeviceNotFound userInfo:nil]);
|
||||
}
|
||||
|
||||
/* Connect to Device */
|
||||
if (lockdownd_client_new_with_handshake(device, &client, "altserver") != LOCKDOWN_E_SUCCESS)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
/* Connect to Installation Proxy */
|
||||
if ((lockdownd_start_service(client, "com.apple.mobile.installation_proxy", &service) != LOCKDOWN_E_SUCCESS) || service == NULL)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
if (instproxy_client_new(device, service, &ipc) != INSTPROXY_E_SUCCESS)
|
||||
{
|
||||
return finish([NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorConnectionFailed userInfo:nil]);
|
||||
}
|
||||
|
||||
if (service)
|
||||
{
|
||||
lockdownd_service_descriptor_free(service);
|
||||
service = NULL;
|
||||
}
|
||||
|
||||
NSUUID *UUID = [NSUUID UUID];
|
||||
__block char *uuidString = (char *)malloc(UUID.UUIDString.length + 1);
|
||||
strncpy(uuidString, (const char *)UUID.UUIDString.UTF8String, UUID.UUIDString.length);
|
||||
uuidString[UUID.UUIDString.length] = '\0';
|
||||
|
||||
self.deletionCompletionHandlers[UUID] = ^(NSError *error) {
|
||||
if (error != nil)
|
||||
{
|
||||
NSString *localizedFailure = [NSString stringWithFormat:NSLocalizedString(@"Could not remove “%@”.", @""), bundleIdentifier];
|
||||
|
||||
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
|
||||
userInfo[NSLocalizedFailureErrorKey] = localizedFailure;
|
||||
|
||||
NSError *localizedError = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
|
||||
finish(localizedError);
|
||||
}
|
||||
else
|
||||
{
|
||||
finish(nil);
|
||||
}
|
||||
|
||||
free(uuidString);
|
||||
};
|
||||
|
||||
instproxy_uninstall(ipc, bundleIdentifier.UTF8String, NULL, ALTDeviceManagerUpdateAppDeletionStatus, uuidString);
|
||||
}
|
||||
|
||||
#pragma mark - Provisioning Profiles -
|
||||
|
||||
- (void)installProvisioningProfiles:(NSSet<ALTProvisioningProfile *> *)provisioningProfiles toDeviceWithUDID:(NSString *)udid activeProvisioningProfiles:(nullable NSSet<NSString *> *)activeProvisioningProfiles completionHandler:(void (^)(BOOL success, NSError *error))completionHandler
|
||||
@@ -1032,7 +1117,7 @@ NSNotificationName const ALTDeviceManagerDeviceDidDisconnectNotification = @"ALT
|
||||
NSString *name = [NSString stringWithCString:device_name encoding:NSUTF8StringEncoding];
|
||||
NSString *identifier = [NSString stringWithCString:udid encoding:NSUTF8StringEncoding];
|
||||
|
||||
ALTDevice *altDevice = [[ALTDevice alloc] initWithName:name identifier:identifier];
|
||||
ALTDevice *altDevice = [[ALTDevice alloc] initWithName:name identifier:identifier type:ALTDeviceTypeiPhone];
|
||||
[connectedDevices addObject:altDevice];
|
||||
|
||||
if (device_name != NULL)
|
||||
@@ -1075,7 +1160,7 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid)
|
||||
{
|
||||
if (code != 0 || name != NULL)
|
||||
{
|
||||
NSLog(@"Error installing app. %@ (%@). %@", @(code), @(name), @(description));
|
||||
NSLog(@"Error installing app. %@ (%@). %@", @(code), @(name ?: ""), @(description ?: ""));
|
||||
|
||||
NSError *error = nil;
|
||||
|
||||
@@ -1085,14 +1170,14 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid)
|
||||
}
|
||||
else
|
||||
{
|
||||
NSString *errorName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
|
||||
NSString *errorName = [NSString stringWithCString:name ?: "" encoding:NSUTF8StringEncoding];
|
||||
if ([errorName isEqualToString:@"DeviceOSVersionTooLow"])
|
||||
{
|
||||
error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorUnsupportediOSVersion userInfo:nil];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSError *underlyingError = [NSError errorWithDomain:AltServerInstallationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: @(description)}];
|
||||
NSError *underlyingError = [NSError errorWithDomain:AltServerInstallationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: @(description ?: "")}];
|
||||
error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorInstallationFailed userInfo:@{NSUnderlyingErrorKey: underlyingError}];
|
||||
}
|
||||
}
|
||||
@@ -1117,6 +1202,43 @@ void ALTDeviceManagerUpdateStatus(plist_t command, plist_t status, void *uuid)
|
||||
}
|
||||
}
|
||||
|
||||
void ALTDeviceManagerUpdateAppDeletionStatus(plist_t command, plist_t status, void *uuid)
|
||||
{
|
||||
NSUUID *UUID = [[NSUUID alloc] initWithUUIDString:[NSString stringWithUTF8String:(const char *)uuid]];
|
||||
|
||||
char *statusName = NULL;
|
||||
instproxy_status_get_name(status, &statusName);
|
||||
|
||||
char *errorName = NULL;
|
||||
char *errorDescription = NULL;
|
||||
uint64_t code = 0;
|
||||
instproxy_status_get_error(status, &errorName, &errorDescription, &code);
|
||||
|
||||
if ([@(statusName) isEqualToString:@"Complete"] || code != 0 || errorName != NULL)
|
||||
{
|
||||
void (^completionHandler)(NSError *) = ALTDeviceManager.sharedManager.deletionCompletionHandlers[UUID];
|
||||
if (completionHandler != nil)
|
||||
{
|
||||
if (code != 0 || errorName != NULL)
|
||||
{
|
||||
NSLog(@"Error removing app. %@ (%@). %@", @(code), @(errorName ?: ""), @(errorDescription ?: ""));
|
||||
|
||||
NSError *underlyingError = [NSError errorWithDomain:AltServerInstallationErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey: @(errorDescription ?: "")}];
|
||||
NSError *error = [NSError errorWithDomain:AltServerErrorDomain code:ALTServerErrorAppDeletionFailed userInfo:@{NSUnderlyingErrorKey: underlyingError}];
|
||||
|
||||
completionHandler(error);
|
||||
}
|
||||
else
|
||||
{
|
||||
NSLog(@"Finished removing app!");
|
||||
completionHandler(nil);
|
||||
}
|
||||
|
||||
ALTDeviceManager.sharedManager.deletionCompletionHandlers[UUID] = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ALTDeviceDidChangeConnectionStatus(const idevice_event_t *event, void *user_data)
|
||||
{
|
||||
ALTDevice * (^deviceForUDID)(NSString *, NSArray<ALTDevice *> *) = ^ALTDevice *(NSString *udid, NSArray<ALTDevice *> *devices) {
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
BF1E312B229F474900370A3C /* ConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3129229F474900370A3C /* ConnectionManager.swift */; };
|
||||
BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E3128229F474900370A3C /* ServerProtocol.swift */; };
|
||||
BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBAC8852295C90300587369 /* Result+Conveniences.swift */; };
|
||||
BF1E315922A061FB00370A3C /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; };
|
||||
BF1E315A22A0620000370A3C /* NSError+ALTServerError.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314922A060F400370A3C /* NSError+ALTServerError.m */; };
|
||||
BF1E315F22A0635900370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
|
||||
BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
|
||||
@@ -35,6 +34,7 @@
|
||||
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */; };
|
||||
BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; };
|
||||
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; };
|
||||
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */; };
|
||||
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */; };
|
||||
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */; };
|
||||
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; };
|
||||
@@ -50,6 +50,9 @@
|
||||
BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */; };
|
||||
BF44CC6C232AEB90004DA9C3 /* LaunchAtLogin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */; };
|
||||
BF44CC6D232AEB90004DA9C3 /* LaunchAtLogin.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF44EEF0246B08BA002A52F2 /* BackupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF44EEEF246B08BA002A52F2 /* BackupController.swift */; };
|
||||
BF44EEF3246B3A17002A52F2 /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = BF44EEF2246B3A17002A52F2 /* AltBackup.ipa */; };
|
||||
BF44EEFC246B4550002A52F2 /* RemoveAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */; };
|
||||
BF458690229872EA00BD7491 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF45868F229872EA00BD7491 /* AppDelegate.swift */; };
|
||||
BF458694229872EA00BD7491 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF458693229872EA00BD7491 /* Assets.xcassets */; };
|
||||
BF458697229872EA00BD7491 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF458695229872EA00BD7491 /* Main.storyboard */; };
|
||||
@@ -118,12 +121,22 @@
|
||||
BF45884B2298D55000BD7491 /* thread.h in Headers */ = {isa = PBXBuildFile; fileRef = BF4588492298D55000BD7491 /* thread.h */; };
|
||||
BF4588882298DD3F00BD7491 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = BF4588872298DD3F00BD7491 /* libxml2.tbd */; };
|
||||
BF4C7F2523801F0800B2556E /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9B63C5229DD44D002F0A62 /* AltSign.framework */; };
|
||||
BF4E8456246F16D700ECCBD4 /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; };
|
||||
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */; };
|
||||
BF56D2AA23DF88310006506D /* AppID.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2A923DF88310006506D /* AppID.swift */; };
|
||||
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */; };
|
||||
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */; };
|
||||
BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF58047D246A28F7008AE704 /* AppDelegate.swift */; };
|
||||
BF580482246A28F7008AE704 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF580481246A28F7008AE704 /* ViewController.swift */; };
|
||||
BF580487246A28F9008AE704 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF580486246A28F9008AE704 /* Assets.xcassets */; };
|
||||
BF58048A246A28F9008AE704 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF580488246A28F9008AE704 /* LaunchScreen.storyboard */; };
|
||||
BF580492246A2C5C008AE704 /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; };
|
||||
BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */; };
|
||||
BF580498246A3D19008AE704 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF580497246A3D19008AE704 /* UIKit.framework */; };
|
||||
BF58049B246A432D008AE704 /* NSError+LocalizedFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */; };
|
||||
BF5C5FCF237DF69100EDD0C6 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; };
|
||||
BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */; };
|
||||
BF6A5320246DC1B0004F59C8 /* FileManager+SharedDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */; };
|
||||
BF6C336224197D700034FD24 /* NSError+LocalizedFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */; };
|
||||
BF6C33652419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */; };
|
||||
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */; };
|
||||
@@ -228,6 +241,7 @@
|
||||
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */; };
|
||||
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */; };
|
||||
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */; };
|
||||
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */; };
|
||||
BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DC22F0E7F3002E24B9 /* Source.swift */; };
|
||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */; };
|
||||
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338E722F10E56002E24B9 /* LaunchViewController.swift */; };
|
||||
@@ -345,6 +359,7 @@
|
||||
BF26A0E02370C5D400F53F9F /* ALTSourceUserInfoKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTSourceUserInfoKey.m; sourceTree = "<group>"; };
|
||||
BF29012E2318F6B100D88A45 /* AppBannerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AppBannerView.xib; sourceTree = "<group>"; };
|
||||
BF2901302318F7A800D88A45 /* AppBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBannerView.swift; sourceTree = "<group>"; };
|
||||
BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAppOperation.swift; sourceTree = "<group>"; };
|
||||
BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchProvisioningProfilesOperation.swift; sourceTree = "<group>"; };
|
||||
BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAppOperation.swift; sourceTree = "<group>"; };
|
||||
BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = "<group>"; };
|
||||
@@ -360,6 +375,9 @@
|
||||
BF43002D22A714AF0051E2BC /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
|
||||
BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+AltStore.swift"; sourceTree = "<group>"; };
|
||||
BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LaunchAtLogin.framework; path = Carthage/Build/Mac/LaunchAtLogin.framework; sourceTree = "<group>"; };
|
||||
BF44EEEF246B08BA002A52F2 /* BackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupController.swift; sourceTree = "<group>"; };
|
||||
BF44EEF2246B3A17002A52F2 /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = AltBackup.ipa; sourceTree = "<group>"; };
|
||||
BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppOperation.swift; sourceTree = "<group>"; };
|
||||
BF45868D229872EA00BD7491 /* AltServer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltServer.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF45868F229872EA00BD7491 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
BF458693229872EA00BD7491 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@@ -441,12 +459,22 @@
|
||||
BF56D2A923DF88310006506D /* AppID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppID.swift; sourceTree = "<group>"; };
|
||||
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppIDsOperation.swift; sourceTree = "<group>"; };
|
||||
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDsViewController.swift; sourceTree = "<group>"; };
|
||||
BF58047B246A28F7008AE704 /* AltBackup.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltBackup.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF58047D246A28F7008AE704 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
BF580481246A28F7008AE704 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
BF580486246A28F9008AE704 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
BF580489246A28F9008AE704 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
BF58048B246A28F9008AE704 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltBackup.swift"; sourceTree = "<group>"; };
|
||||
BF580497246A3D19008AE704 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
|
||||
BF580499246A4153008AE704 /* AltBackup.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltBackup.entitlements; sourceTree = "<group>"; };
|
||||
BF5AB3A72285FE6C00DC914B /* AltSign.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AltPlugin.mailbundle; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF5C5FC7237DF5AE00EDD0C6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BF5C5FCD237DF69100EDD0C6 /* ALTPluginService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTPluginService.h; sourceTree = "<group>"; };
|
||||
BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPluginService.m; sourceTree = "<group>"; };
|
||||
BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+DirectorySize.swift"; sourceTree = "<group>"; };
|
||||
BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+SharedDirectories.swift"; sourceTree = "<group>"; };
|
||||
BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+LocalizedFailure.swift"; sourceTree = "<group>"; };
|
||||
BF6C33632419ADEB0034FD24 /* AltStore 5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 5.xcdatamodel"; sourceTree = "<group>"; };
|
||||
BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore4ToAltStore5.xcmappingmodel; sourceTree = "<group>"; };
|
||||
@@ -568,6 +596,7 @@
|
||||
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = "<group>"; };
|
||||
BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationError.swift; sourceTree = "<group>"; };
|
||||
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendAppOperation.swift; sourceTree = "<group>"; };
|
||||
BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppBackupOperation.swift; sourceTree = "<group>"; };
|
||||
BFE338DC22F0E7F3002E24B9 /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = "<group>"; };
|
||||
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchSourceOperation.swift; sourceTree = "<group>"; };
|
||||
BFE338E722F10E56002E24B9 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; };
|
||||
@@ -622,6 +651,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF580478246A28F7008AE704 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BF580498246A3D19008AE704 /* UIKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF5C5FC2237DF5AE00EDD0C6 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -902,6 +939,21 @@
|
||||
path = "App IDs";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BF58047C246A28F7008AE704 /* AltBackup */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF580499246A4153008AE704 /* AltBackup.entitlements */,
|
||||
BF58047D246A28F7008AE704 /* AppDelegate.swift */,
|
||||
BF580481246A28F7008AE704 /* ViewController.swift */,
|
||||
BF44EEEF246B08BA002A52F2 /* BackupController.swift */,
|
||||
BF580495246A3CB5008AE704 /* UIColor+AltBackup.swift */,
|
||||
BF580486246A28F9008AE704 /* Assets.xcassets */,
|
||||
BF580488246A28F9008AE704 /* LaunchScreen.storyboard */,
|
||||
BF58048B246A28F9008AE704 /* Info.plist */,
|
||||
);
|
||||
path = AltBackup;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BF5C5FC6237DF5AE00EDD0C6 /* AltPlugin */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1019,6 +1071,7 @@
|
||||
BF1E315122A0616100370A3C /* AltKit */,
|
||||
BF45872C2298D31600BD7491 /* libimobiledevice */,
|
||||
BF5C5FC6237DF5AE00EDD0C6 /* AltPlugin */,
|
||||
BF58047C246A28F7008AE704 /* AltBackup */,
|
||||
BFD247852284BB3300981D42 /* Frameworks */,
|
||||
BFD2476B2284B9A500981D42 /* Products */,
|
||||
4460E048E3AC1C9708C4FA33 /* Pods */,
|
||||
@@ -1033,6 +1086,7 @@
|
||||
BF45872B2298D31600BD7491 /* libimobiledevice.a */,
|
||||
BF1E315022A0616100370A3C /* libAltKit.a */,
|
||||
BF5C5FC5237DF5AE00EDD0C6 /* AltPlugin.mailbundle */,
|
||||
BF58047B246A28F7008AE704 /* AltBackup.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -1073,6 +1127,7 @@
|
||||
BFD247852284BB3300981D42 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF580497246A3D19008AE704 /* UIKit.framework */,
|
||||
BF44CC6A232AEB74004DA9C3 /* LaunchAtLogin.framework */,
|
||||
BF9B63C5229DD44D002F0A62 /* AltSign.framework */,
|
||||
BF4588962298DE6E00BD7491 /* libzip.framework */,
|
||||
@@ -1118,6 +1173,7 @@
|
||||
BFD247962284D7C100981D42 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF44EEF2246B3A17002A52F2 /* AltBackup.ipa */,
|
||||
BFB1169C22932DB100BB457C /* apps.json */,
|
||||
BFD247762284B9A700981D42 /* Assets.xcassets */,
|
||||
BF770E6822BD57DD002A40FE /* Silence.m4a */,
|
||||
@@ -1168,6 +1224,7 @@
|
||||
BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */,
|
||||
BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */,
|
||||
BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */,
|
||||
BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -1245,6 +1302,9 @@
|
||||
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
|
||||
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */,
|
||||
BFCCB519245E3401001853EA /* VerifyAppOperation.swift */,
|
||||
BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */,
|
||||
BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */,
|
||||
BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */,
|
||||
);
|
||||
path = Operations;
|
||||
sourceTree = "<group>";
|
||||
@@ -1370,6 +1430,23 @@
|
||||
productReference = BF45872B2298D31600BD7491 /* libimobiledevice.a */;
|
||||
productType = "com.apple.product-type.library.static";
|
||||
};
|
||||
BF58047A246A28F7008AE704 /* AltBackup */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BF58048E246A28F9008AE704 /* Build configuration list for PBXNativeTarget "AltBackup" */;
|
||||
buildPhases = (
|
||||
BF580477246A28F7008AE704 /* Sources */,
|
||||
BF580478246A28F7008AE704 /* Frameworks */,
|
||||
BF580479246A28F7008AE704 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = AltBackup;
|
||||
productName = AltBackup;
|
||||
productReference = BF58047B246A28F7008AE704 /* AltBackup.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = BF5C5FC8237DF5AE00EDD0C6 /* Build configuration list for PBXNativeTarget "AltPlugin" */;
|
||||
@@ -1414,7 +1491,7 @@
|
||||
BFD247622284B9A500981D42 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1120;
|
||||
LastSwiftUpdateCheck = 1140;
|
||||
LastUpgradeCheck = 1020;
|
||||
ORGANIZATIONNAME = "Riley Testut";
|
||||
TargetAttributes = {
|
||||
@@ -1436,6 +1513,9 @@
|
||||
BF45872A2298D31600BD7491 = {
|
||||
CreatedOnToolsVersion = 10.2.1;
|
||||
};
|
||||
BF58047A246A28F7008AE704 = {
|
||||
CreatedOnToolsVersion = 11.4.1;
|
||||
};
|
||||
BF5C5FC4237DF5AE00EDD0C6 = {
|
||||
CreatedOnToolsVersion = 11.2;
|
||||
LastSwiftMigration = 1120;
|
||||
@@ -1472,6 +1552,7 @@
|
||||
BF1E314F22A0616100370A3C /* AltKit */,
|
||||
BF45872A2298D31600BD7491 /* libimobiledevice */,
|
||||
BF5C5FC4237DF5AE00EDD0C6 /* AltPlugin */,
|
||||
BF58047A246A28F7008AE704 /* AltBackup */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -1486,6 +1567,15 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF580479246A28F7008AE704 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BF58048A246A28F9008AE704 /* LaunchScreen.storyboard in Resources */,
|
||||
BF580487246A28F9008AE704 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF5C5FC3237DF5AE00EDD0C6 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -1501,6 +1591,7 @@
|
||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
|
||||
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */,
|
||||
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
||||
BF44EEF3246B3A17002A52F2 /* AltBackup.ipa in Resources */,
|
||||
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */,
|
||||
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */,
|
||||
BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */,
|
||||
@@ -1639,11 +1730,11 @@
|
||||
files = (
|
||||
BF718BD823C93DB700A89F2D /* AltKit.m in Sources */,
|
||||
BF1E315A22A0620000370A3C /* NSError+ALTServerError.m in Sources */,
|
||||
BF1E315922A061FB00370A3C /* Bundle+AltStore.swift in Sources */,
|
||||
BF1E315822A061F900370A3C /* Result+Conveniences.swift in Sources */,
|
||||
BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */,
|
||||
BFD44606241188C400EAB90A /* CodableServerError.swift in Sources */,
|
||||
BF1E315722A061F500370A3C /* ServerProtocol.swift in Sources */,
|
||||
BF4E8456246F16D700ECCBD4 /* Bundle+AltStore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1724,6 +1815,19 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF580477246A28F7008AE704 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BF580492246A2C5C008AE704 /* Bundle+AltStore.swift in Sources */,
|
||||
BF580496246A3CB5008AE704 /* UIColor+AltBackup.swift in Sources */,
|
||||
BF580482246A28F7008AE704 /* ViewController.swift in Sources */,
|
||||
BF44EEF0246B08BA002A52F2 /* BackupController.swift in Sources */,
|
||||
BF58049B246A432D008AE704 /* NSError+LocalizedFailure.swift in Sources */,
|
||||
BF58047E246A28F7008AE704 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
BF5C5FC1237DF5AE00EDD0C6 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -1780,6 +1884,7 @@
|
||||
BF41B806233423AE00C593A3 /* TabBarController.swift in Sources */,
|
||||
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */,
|
||||
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */,
|
||||
BF44EEFC246B4550002A52F2 /* RemoveAppOperation.swift in Sources */,
|
||||
BF100C54232D7DAE006A8926 /* StoreAppPolicy.swift in Sources */,
|
||||
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
|
||||
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
||||
@@ -1823,7 +1928,9 @@
|
||||
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
|
||||
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */,
|
||||
BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */,
|
||||
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */,
|
||||
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
|
||||
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */,
|
||||
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
||||
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
|
||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
|
||||
@@ -1839,6 +1946,7 @@
|
||||
BF770E5622BC3C03002A40FE /* Server.swift in Sources */,
|
||||
BFA8172923C56042001B5953 /* ServerConnection.swift in Sources */,
|
||||
BF56D2AF23DF9E310006506D /* AppIDsViewController.swift in Sources */,
|
||||
BF6A5320246DC1B0004F59C8 /* FileManager+SharedDirectories.swift in Sources */,
|
||||
BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1877,6 +1985,14 @@
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BF580488246A28F9008AE704 /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
BF580489246A28F9008AE704 /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFD247732284B9A500981D42 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
@@ -1940,7 +2056,7 @@
|
||||
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -1973,7 +2089,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14.4;
|
||||
MARKETING_VERSION = 1.3;
|
||||
MARKETING_VERSION = 1.3.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
@@ -1993,7 +2109,7 @@
|
||||
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
CURRENT_PROJECT_VERSION = 21;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -2026,7 +2142,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14.4;
|
||||
MARKETING_VERSION = 1.3;
|
||||
MARKETING_VERSION = 1.3.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltServer;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
@@ -2109,6 +2225,47 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BF58048C246A28F9008AE704 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = AltBackup/AltBackup.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
INFOPLIST_FILE = AltBackup/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltBackup;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
BF58048D246A28F9008AE704 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = AltBackup/AltBackup.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
INFOPLIST_FILE = AltBackup/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltBackup;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BF5C5FC9237DF5AE00EDD0C6 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -2300,7 +2457,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.2;
|
||||
MARKETING_VERSION = 1.3.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2328,7 +2485,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.2;
|
||||
MARKETING_VERSION = 1.3.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2369,6 +2526,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
BF58048E246A28F9008AE704 /* Build configuration list for PBXNativeTarget "AltBackup" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
BF58048C246A28F9008AE704 /* Debug */,
|
||||
BF58048D246A28F9008AE704 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
BF5C5FC8237DF5AE00EDD0C6 /* Build configuration list for PBXNativeTarget "AltPlugin" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
@@ -4,5 +4,9 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.rileytestut.AltStore</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -55,7 +55,10 @@ extension AppDelegate
|
||||
static let openPatreonSettingsDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.OpenPatreonSettingsDeepLinkNotification")
|
||||
static let importAppDeepLinkNotification = Notification.Name("com.rileytestut.AltStore.ImportAppDeepLinkNotification")
|
||||
|
||||
static let appBackupDidFinish = Notification.Name("com.rileytestut.AltStore.AppBackupDidFinish")
|
||||
|
||||
static let importAppDeepLinkURLKey = "fileURL"
|
||||
static let appBackupResultKey = "result"
|
||||
}
|
||||
|
||||
@UIApplicationMain
|
||||
@@ -134,13 +137,53 @@ private extension AppDelegate
|
||||
else
|
||||
{
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return false }
|
||||
guard let host = components.host, host.lowercased() == "patreon" else { return false }
|
||||
guard let host = components.host?.lowercased() else { return false }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||
switch host
|
||||
{
|
||||
case "patreon":
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
case "appbackupresponse":
|
||||
let result: Result<Void, Error>
|
||||
|
||||
switch url.path.lowercased()
|
||||
{
|
||||
case "/success": result = .success(())
|
||||
case "/failure":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value } ?? [:]
|
||||
guard
|
||||
let errorDomain = queryItems["errorDomain"],
|
||||
let errorCodeString = queryItems["errorCode"], let errorCode = Int(errorCodeString),
|
||||
let errorDescription = queryItems["errorDescription"]
|
||||
else { return false }
|
||||
|
||||
let error = NSError(domain: errorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: errorDescription])
|
||||
result = .failure(error)
|
||||
|
||||
default: return false
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: AppDelegate.appBackupDidFinish, object: nil, userInfo: [AppDelegate.appBackupResultKey: result])
|
||||
|
||||
return true
|
||||
|
||||
case "install":
|
||||
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
|
||||
guard let downloadURLString = queryItems["url"], let downloadURL = URL(string: downloadURLString) else { return false }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: AppDelegate.importAppDeepLinkNotification, object: nil, userInfo: [AppDelegate.importAppDeepLinkURLKey: downloadURL])
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
default: return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
AltStore/Extensions/FileManager+SharedDirectories.swift
Normal file
32
AltStore/Extensions/FileManager+SharedDirectories.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// FileManager+SharedDirectories.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/14/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltKit
|
||||
|
||||
extension FileManager
|
||||
{
|
||||
var altstoreSharedDirectory: URL? {
|
||||
guard let appGroup = Bundle.main.appGroups.first else { return nil }
|
||||
|
||||
let sharedDirectoryURL = self.containerURL(forSecurityApplicationGroupIdentifier: appGroup)
|
||||
return sharedDirectoryURL
|
||||
}
|
||||
|
||||
var appBackupsDirectory: URL? {
|
||||
let appBackupsDirectory = self.altstoreSharedDirectory?.appendingPathComponent("Backups", isDirectory: true)
|
||||
return appBackupsDirectory
|
||||
}
|
||||
|
||||
func backupDirectoryURL(for app: InstalledApp) -> URL?
|
||||
{
|
||||
let backupDirectoryURL = self.appBackupsDirectory?.appendingPathComponent(app.bundleIdentifier, isDirectory: true)
|
||||
return backupDirectoryURL
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ extension NSError
|
||||
{
|
||||
var userInfo = self.userInfo
|
||||
userInfo[NSLocalizedFailureErrorKey] = failure
|
||||
userInfo[NSLocalizedDescriptionKey] = self.localizedDescription
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = self.localizedFailureReason
|
||||
userInfo[NSLocalizedRecoverySuggestionErrorKey] = self.localizedRecoverySuggestion
|
||||
|
||||
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
|
||||
return error
|
||||
|
||||
@@ -22,6 +22,9 @@ extension UserDefaults
|
||||
|
||||
@NSManaged var legacySideloadedApps: [String]?
|
||||
|
||||
@NSManaged var isLegacyDeactivationSupported: Bool
|
||||
@NSManaged var activeAppLimitIncludesExtensions: Bool
|
||||
|
||||
var activeAppsLimit: Int? {
|
||||
get {
|
||||
return self._activeAppsLimit?.intValue
|
||||
@@ -41,6 +44,14 @@ extension UserDefaults
|
||||
|
||||
func registerDefaults()
|
||||
{
|
||||
self.register(defaults: [#keyPath(UserDefaults.isBackgroundRefreshEnabled): true])
|
||||
let ios13_5 = OperatingSystemVersion(majorVersion: 13, minorVersion: 5, patchVersion: 0)
|
||||
let isLegacyDeactivationSupported = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5)
|
||||
let activeAppLimitIncludesExtensions = !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios13_5)
|
||||
|
||||
self.register(defaults: [
|
||||
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
|
||||
#keyPath(UserDefaults.isLegacyDeactivationSupported): isLegacyDeactivationSupported,
|
||||
#keyPath(UserDefaults.activeAppLimitIncludesExtensions): activeAppLimitIncludesExtensions
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ALTAppGroups</key>
|
||||
<array>
|
||||
<string>group.com.rileytestut.AltStore</string>
|
||||
</array>
|
||||
<key>ALTDeviceID</key>
|
||||
<string>00008030-001948590202802E</string>
|
||||
<key>ALTServerID</key>
|
||||
@@ -49,6 +53,16 @@
|
||||
<string>altstore</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>AltStore Backup</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>altstore-com.rileytestut.AltStore</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
|
||||
@@ -72,6 +72,17 @@ extension AppManager
|
||||
continue
|
||||
}
|
||||
|
||||
guard !self.isActivelyManagingApp(withBundleID: app.bundleIdentifier) else { continue }
|
||||
|
||||
if !UserDefaults.standard.isLegacyDeactivationSupported
|
||||
{
|
||||
// We can't (ab)use provisioning profiles to deactivate apps,
|
||||
// which means we must delete apps to free up active slots.
|
||||
// So, only check if active apps are installed to prevent
|
||||
// false positives when checking inactive apps.
|
||||
guard app.isActive else { continue }
|
||||
}
|
||||
|
||||
let uti = UTTypeCopyDeclaration(app.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
||||
if uti == nil && !legacySideloadedApps.contains(app.bundleIdentifier)
|
||||
{
|
||||
@@ -103,7 +114,7 @@ extension AppManager
|
||||
let resourceValues = try appDirectory.resourceValues(forKeys: [.isDirectoryKey, .nameKey])
|
||||
guard let isDirectory = resourceValues.isDirectory, let bundleID = resourceValues.name else { continue }
|
||||
|
||||
if isDirectory && !installedAppBundleIDs.contains(bundleID) && !self.installationProgress.keys.contains(bundleID)
|
||||
if isDirectory && !installedAppBundleIDs.contains(bundleID) && !self.isActivelyManagingApp(withBundleID: bundleID)
|
||||
{
|
||||
print("DELETING CACHED APP:", bundleID)
|
||||
try FileManager.default.removeItem(at: appDirectory)
|
||||
@@ -308,13 +319,19 @@ extension AppManager
|
||||
|
||||
func activate(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||
{
|
||||
let group = self.refresh([installedApp], presentingViewController: presentingViewController)
|
||||
let group = RefreshGroup()
|
||||
|
||||
let operation = AppOperation.activate(installedApp)
|
||||
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||
|
||||
group.completionHandler = { (results) in
|
||||
do
|
||||
{
|
||||
guard let result = results.values.first else { throw OperationError.unknown }
|
||||
|
||||
let installedApp = try result.get()
|
||||
assert(installedApp.managedObjectContext != nil)
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
installedApp.isActive = true
|
||||
completionHandler(.success(installedApp))
|
||||
@@ -327,19 +344,142 @@ extension AppManager
|
||||
}
|
||||
}
|
||||
|
||||
func deactivate(_ installedApp: InstalledApp, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||
func deactivate(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||
{
|
||||
let context = OperationContext()
|
||||
|
||||
let findServerOperation = self.findServer(context: context) { _ in }
|
||||
|
||||
let deactivateAppOperation = DeactivateAppOperation(app: installedApp, context: context)
|
||||
deactivateAppOperation.resultHandler = { (result) in
|
||||
completionHandler(result)
|
||||
if UserDefaults.standard.isLegacyDeactivationSupported
|
||||
{
|
||||
// Normally we pipe everything down into perform(),
|
||||
// but the pre-iOS 13.5 deactivation method doesn't require
|
||||
// authentication, so we keep it separate.
|
||||
let context = OperationContext()
|
||||
|
||||
let findServerOperation = self.findServer(context: context) { _ in }
|
||||
|
||||
let deactivateAppOperation = DeactivateAppOperation(app: installedApp, context: context)
|
||||
deactivateAppOperation.resultHandler = { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
deactivateAppOperation.addDependency(findServerOperation)
|
||||
|
||||
self.run([deactivateAppOperation], context: context, requiresSerialQueue: true)
|
||||
}
|
||||
else
|
||||
{
|
||||
let group = RefreshGroup()
|
||||
group.completionHandler = { (results) in
|
||||
do
|
||||
{
|
||||
guard let result = results.values.first else { throw OperationError.unknown }
|
||||
|
||||
let installedApp = try result.get()
|
||||
assert(installedApp.managedObjectContext != nil)
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
completionHandler(.success(installedApp))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
let operation = AppOperation.deactivate(installedApp)
|
||||
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||
}
|
||||
}
|
||||
|
||||
func backup(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||
{
|
||||
let group = RefreshGroup()
|
||||
group.completionHandler = { (results) in
|
||||
do
|
||||
{
|
||||
guard let result = results.values.first else { throw OperationError.unknown }
|
||||
|
||||
let installedApp = try result.get()
|
||||
assert(installedApp.managedObjectContext != nil)
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
completionHandler(.success(installedApp))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
deactivateAppOperation.addDependency(findServerOperation)
|
||||
|
||||
self.run([deactivateAppOperation], context: context, requiresSerialQueue: true)
|
||||
let operation = AppOperation.backup(installedApp)
|
||||
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||
}
|
||||
|
||||
func restore(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||
{
|
||||
let group = RefreshGroup()
|
||||
group.completionHandler = { (results) in
|
||||
do
|
||||
{
|
||||
guard let result = results.values.first else { throw OperationError.unknown }
|
||||
|
||||
let installedApp = try result.get()
|
||||
assert(installedApp.managedObjectContext != nil)
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
installedApp.isActive = true
|
||||
completionHandler(.success(installedApp))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
let operation = AppOperation.restore(installedApp)
|
||||
self.perform([operation], presentingViewController: presentingViewController, group: group)
|
||||
}
|
||||
|
||||
func remove(_ installedApp: InstalledApp, completionHandler: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let authenticationContext = AuthenticatedOperationContext()
|
||||
let appContext = InstallAppOperationContext(bundleIdentifier: installedApp.bundleIdentifier, authenticatedContext: authenticationContext)
|
||||
appContext.installedApp = installedApp
|
||||
|
||||
let removeAppOperation = RSTAsyncBlockOperation { (operation) in
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let installedApp = context.object(with: installedApp.objectID) as! InstalledApp
|
||||
context.delete(installedApp)
|
||||
|
||||
do { try context.save() }
|
||||
catch { appContext.error = error }
|
||||
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
|
||||
let removeAppBackupOperation = RemoveAppBackupOperation(context: appContext)
|
||||
removeAppBackupOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error): print("Failed to remove app backup.", error)
|
||||
}
|
||||
|
||||
// Throw the error from removeAppOperation,
|
||||
// since that's the error we really care about.
|
||||
if let error = appContext.error
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
removeAppBackupOperation.addDependency(removeAppOperation)
|
||||
|
||||
self.run([removeAppOperation, removeAppBackupOperation], context: authenticationContext)
|
||||
}
|
||||
|
||||
func installationProgress(for app: AppProtocol) -> Progress?
|
||||
@@ -361,12 +501,19 @@ private extension AppManager
|
||||
{
|
||||
case install(AppProtocol)
|
||||
case update(AppProtocol)
|
||||
case refresh(AppProtocol)
|
||||
case refresh(InstalledApp)
|
||||
case activate(InstalledApp)
|
||||
case deactivate(InstalledApp)
|
||||
case backup(InstalledApp)
|
||||
case restore(InstalledApp)
|
||||
|
||||
var app: AppProtocol {
|
||||
switch self
|
||||
{
|
||||
case .install(let app), .update(let app), .refresh(let app): return app
|
||||
case .install(let app), .update(let app), .refresh(let app as AppProtocol),
|
||||
.activate(let app as AppProtocol), .deactivate(let app as AppProtocol),
|
||||
.backup(let app as AppProtocol), .restore(let app as AppProtocol):
|
||||
return app
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +533,12 @@ private extension AppManager
|
||||
}
|
||||
}
|
||||
|
||||
func isActivelyManagingApp(withBundleID bundleID: String) -> Bool
|
||||
{
|
||||
let isActivelyManaging = self.installationProgress.keys.contains(bundleID) || self.refreshProgress.keys.contains(bundleID)
|
||||
return isActivelyManaging
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func perform(_ operations: [AppOperation], presentingViewController: UIViewController?, group: RefreshGroup) -> RefreshGroup
|
||||
{
|
||||
@@ -435,22 +588,63 @@ private extension AppManager
|
||||
|
||||
switch operation
|
||||
{
|
||||
case .refresh(let installedApp as InstalledApp) where installedApp.certificateSerialNumber == group.context.certificate?.serialNumber:
|
||||
// Refreshing apps, but using same certificate as last time, so we can just refresh provisioning profiles.
|
||||
|
||||
let refreshProgress = self._refresh(installedApp, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
|
||||
|
||||
case .refresh(let app), .install(let app), .update(let app):
|
||||
// Either installing for first time, or refreshing with a different signing certificate,
|
||||
// so we need to resign the app then install it.
|
||||
|
||||
case .install(let app), .update(let app):
|
||||
let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(installProgress, withPendingUnitCount: 80)
|
||||
|
||||
case .activate(let app) where UserDefaults.standard.isLegacyDeactivationSupported: fallthrough
|
||||
case .refresh(let app):
|
||||
// Check if backup app is installed in place of real app.
|
||||
let uti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
||||
|
||||
if app.certificateSerialNumber == group.context.certificate?.serialNumber && uti == nil
|
||||
{
|
||||
// Refreshing with same certificate as last time, and backup app isn't still installed,
|
||||
// so we can just refresh provisioning profiles.
|
||||
|
||||
let refreshProgress = self._refresh(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Refreshing using different certificate or backup app is still installed,
|
||||
// so we need to resign + install.
|
||||
|
||||
let installProgress = self._install(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(installProgress, withPendingUnitCount: 80)
|
||||
}
|
||||
|
||||
case .activate(let app):
|
||||
let activateProgress = self._activate(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(activateProgress, withPendingUnitCount: 80)
|
||||
|
||||
case .deactivate(let app):
|
||||
let deactivateProgress = self._deactivate(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(deactivateProgress, withPendingUnitCount: 80)
|
||||
|
||||
case .backup(let app):
|
||||
let backupProgress = self._backup(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(backupProgress, withPendingUnitCount: 80)
|
||||
|
||||
case .restore(let app):
|
||||
// Restoring, which is effectively just activating an app.
|
||||
|
||||
let activateProgress = self._activate(app, operation: operation, group: group) { (result) in
|
||||
self.finish(operation, result: result, group: group, progress: progress)
|
||||
}
|
||||
progress?.addChild(activateProgress, withPendingUnitCount: 80)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,11 +672,13 @@ private extension AppManager
|
||||
return group
|
||||
}
|
||||
|
||||
private func _install(_ app: AppProtocol, operation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
private func _install(_ app: AppProtocol, operation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any]? = nil, cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
let context = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
let context = context ?? InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
assert(context.authenticatedContext === group.context)
|
||||
|
||||
context.beginInstallationHandler = { (installedApp) in
|
||||
switch operation
|
||||
{
|
||||
@@ -506,17 +702,29 @@ private extension AppManager
|
||||
downloadingApp = storeApp
|
||||
}
|
||||
|
||||
let downloadedAppURL = context.temporaryDirectory.appendingPathComponent("Cached.app")
|
||||
|
||||
/* Download */
|
||||
let downloadOperation = DownloadAppOperation(app: downloadingApp, context: context)
|
||||
let downloadOperation = DownloadAppOperation(app: downloadingApp, destinationURL: downloadedAppURL, context: context)
|
||||
downloadOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
do
|
||||
{
|
||||
case .failure(let error): context.error = error
|
||||
case .success(let app): context.app = app
|
||||
let app = try result.get()
|
||||
context.app = app
|
||||
|
||||
if cacheApp
|
||||
{
|
||||
try FileManager.default.copyItem(at: app.fileURL, to: InstalledApp.fileURL(for: app), shouldReplace: true)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.error = error
|
||||
}
|
||||
}
|
||||
progress.addChild(downloadOperation.progress, withPendingUnitCount: 25)
|
||||
|
||||
|
||||
/* Verify App */
|
||||
let verifyOperation = VerifyAppOperation(context: context)
|
||||
verifyOperation.resultHandler = { (result) in
|
||||
@@ -528,6 +736,7 @@ private extension AppManager
|
||||
}
|
||||
verifyOperation.addDependency(downloadOperation)
|
||||
|
||||
|
||||
/* Refresh Anisette Data */
|
||||
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
|
||||
refreshAnisetteDataOperation.resultHandler = { (result) in
|
||||
@@ -542,6 +751,7 @@ private extension AppManager
|
||||
|
||||
/* Fetch Provisioning Profiles */
|
||||
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
||||
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
|
||||
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
@@ -586,6 +796,8 @@ private extension AppManager
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let installedApp):
|
||||
context.installedApp = installedApp
|
||||
|
||||
if let app = app as? StoreApp, let storeApp = installedApp.managedObjectContext?.object(with: app.objectID) as? StoreApp
|
||||
{
|
||||
installedApp.storeApp = storeApp
|
||||
@@ -616,7 +828,7 @@ private extension AppManager
|
||||
|
||||
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
context.app = ALTApplication(fileURL: app.url)
|
||||
|
||||
|
||||
/* Fetch Provisioning Profiles */
|
||||
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
|
||||
fetchProvisioningProfilesOperation.resultHandler = { (result) in
|
||||
@@ -627,7 +839,7 @@ private extension AppManager
|
||||
}
|
||||
}
|
||||
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60)
|
||||
|
||||
|
||||
/* Refresh */
|
||||
let refreshAppOperation = RefreshAppOperation(context: context)
|
||||
refreshAppOperation.resultHandler = { (result) in
|
||||
@@ -656,6 +868,351 @@ private extension AppManager
|
||||
let operations = [fetchProvisioningProfilesOperation, refreshAppOperation]
|
||||
group.add(operations)
|
||||
self.run(operations, context: group.context)
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
private func _activate(_ app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
let restoreContext = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
let appContext = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
|
||||
let installBackupAppProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let installBackupAppOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||
app.managedObjectContext?.perform {
|
||||
guard let self = self else { return }
|
||||
|
||||
let progress = self._installBackupApp(for: app, operation: appOperation, group: group, context: restoreContext) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success(let installedApp): restoreContext.installedApp = installedApp
|
||||
case .failure(let error):
|
||||
restoreContext.error = error
|
||||
appContext.error = error
|
||||
}
|
||||
|
||||
operation.finish()
|
||||
}
|
||||
installBackupAppProgress.addChild(progress, withPendingUnitCount: 100)
|
||||
}
|
||||
}
|
||||
progress.addChild(installBackupAppProgress, withPendingUnitCount: 30)
|
||||
|
||||
let restoreAppOperation = BackupAppOperation(action: .restore, context: restoreContext)
|
||||
restoreAppOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error):
|
||||
restoreContext.error = error
|
||||
appContext.error = error
|
||||
}
|
||||
}
|
||||
restoreAppOperation.addDependency(installBackupAppOperation)
|
||||
progress.addChild(restoreAppOperation.progress, withPendingUnitCount: 15)
|
||||
|
||||
let installAppProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let installAppOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||
app.managedObjectContext?.perform {
|
||||
guard let self = self else { return }
|
||||
|
||||
let progress = self._install(app, operation: appOperation, group: group, context: appContext) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success(let installedApp): appContext.installedApp = installedApp
|
||||
case .failure(let error): appContext.error = error
|
||||
}
|
||||
|
||||
operation.finish()
|
||||
}
|
||||
installAppProgress.addChild(progress, withPendingUnitCount: 100)
|
||||
}
|
||||
}
|
||||
installAppOperation.addDependency(restoreAppOperation)
|
||||
progress.addChild(installAppProgress, withPendingUnitCount: 50)
|
||||
|
||||
let cleanUpProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let cleanUpOperation = RSTAsyncBlockOperation { (operation) in
|
||||
do
|
||||
{
|
||||
let installedApp = try Result(appContext.installedApp, appContext.error).get()
|
||||
|
||||
var result: Result<Void, Error>!
|
||||
installedApp.managedObjectContext?.performAndWait {
|
||||
result = Result { try installedApp.managedObjectContext?.save() }
|
||||
}
|
||||
try result.get()
|
||||
|
||||
// Successfully saved, so _now_ we can remove backup.
|
||||
|
||||
let removeAppBackupOperation = RemoveAppBackupOperation(context: appContext)
|
||||
removeAppBackupOperation.resultHandler = { (result) in
|
||||
installedApp.managedObjectContext?.perform {
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
// Don't report error, since it doesn't really matter.
|
||||
print("Failed to delete app backup.", error)
|
||||
|
||||
case .success: break
|
||||
}
|
||||
|
||||
completionHandler(.success(installedApp))
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
cleanUpProgress.addChild(removeAppBackupOperation.progress, withPendingUnitCount: 100)
|
||||
|
||||
group.add([removeAppBackupOperation])
|
||||
self.run([removeAppBackupOperation], context: group.context)
|
||||
}
|
||||
catch let error where restoreContext.installedApp != nil
|
||||
{
|
||||
// Activation failed, but restore app was installed, so remove the app.
|
||||
|
||||
// Remove error so operation doesn't quit early,
|
||||
restoreContext.error = nil
|
||||
|
||||
let removeAppOperation = RemoveAppOperation(context: restoreContext)
|
||||
removeAppOperation.resultHandler = { (result) in
|
||||
completionHandler(.failure(error))
|
||||
operation.finish()
|
||||
}
|
||||
cleanUpProgress.addChild(removeAppOperation.progress, withPendingUnitCount: 100)
|
||||
|
||||
group.add([removeAppOperation])
|
||||
self.run([removeAppOperation], context: group.context)
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Activation failed.
|
||||
completionHandler(.failure(error))
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
cleanUpOperation.addDependency(installAppOperation)
|
||||
progress.addChild(cleanUpProgress, withPendingUnitCount: 5)
|
||||
|
||||
group.add([installBackupAppOperation, restoreAppOperation, installAppOperation, cleanUpOperation])
|
||||
self.run([installBackupAppOperation, installAppOperation, restoreAppOperation, cleanUpOperation], context: group.context)
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
private func _deactivate(_ app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let context = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
|
||||
let installBackupAppProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let installBackupAppOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||
app.managedObjectContext?.perform {
|
||||
guard let self = self else { return }
|
||||
|
||||
let progress = self._installBackupApp(for: app, operation: appOperation, group: group, context: context) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success(let installedApp): context.installedApp = installedApp
|
||||
case .failure(let error): context.error = error
|
||||
}
|
||||
|
||||
operation.finish()
|
||||
}
|
||||
installBackupAppProgress.addChild(progress, withPendingUnitCount: 100)
|
||||
}
|
||||
}
|
||||
progress.addChild(installBackupAppProgress, withPendingUnitCount: 70)
|
||||
|
||||
let backupAppOperation = BackupAppOperation(action: .backup, context: context)
|
||||
backupAppOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): context.error = error
|
||||
case .success: break
|
||||
}
|
||||
}
|
||||
backupAppOperation.addDependency(installBackupAppOperation)
|
||||
progress.addChild(backupAppOperation.progress, withPendingUnitCount: 15)
|
||||
|
||||
let removeAppOperation = RemoveAppOperation(context: context)
|
||||
removeAppOperation.resultHandler = { (result) in
|
||||
completionHandler(result)
|
||||
}
|
||||
removeAppOperation.addDependency(backupAppOperation)
|
||||
progress.addChild(removeAppOperation.progress, withPendingUnitCount: 15)
|
||||
|
||||
group.add([installBackupAppOperation, backupAppOperation, removeAppOperation])
|
||||
self.run([installBackupAppOperation, backupAppOperation, removeAppOperation], context: group.context)
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
private func _backup(_ app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
let restoreContext = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
let appContext = InstallAppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
|
||||
|
||||
let installBackupAppProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let installBackupAppOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||
app.managedObjectContext?.perform {
|
||||
guard let self = self else { return }
|
||||
|
||||
let progress = self._installBackupApp(for: app, operation: appOperation, group: group, context: restoreContext) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success(let installedApp): restoreContext.installedApp = installedApp
|
||||
case .failure(let error):
|
||||
restoreContext.error = error
|
||||
appContext.error = error
|
||||
}
|
||||
|
||||
operation.finish()
|
||||
}
|
||||
installBackupAppProgress.addChild(progress, withPendingUnitCount: 100)
|
||||
}
|
||||
}
|
||||
progress.addChild(installBackupAppProgress, withPendingUnitCount: 30)
|
||||
|
||||
let backupAppOperation = BackupAppOperation(action: .backup, context: restoreContext)
|
||||
backupAppOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error):
|
||||
restoreContext.error = error
|
||||
appContext.error = error
|
||||
}
|
||||
}
|
||||
backupAppOperation.addDependency(installBackupAppOperation)
|
||||
progress.addChild(backupAppOperation.progress, withPendingUnitCount: 15)
|
||||
|
||||
let installAppProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let installAppOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||
app.managedObjectContext?.perform {
|
||||
guard let self = self else { return }
|
||||
|
||||
let progress = self._install(app, operation: appOperation, group: group, context: appContext) { (result) in
|
||||
completionHandler(result)
|
||||
operation.finish()
|
||||
}
|
||||
installAppProgress.addChild(progress, withPendingUnitCount: 100)
|
||||
}
|
||||
}
|
||||
installAppOperation.addDependency(backupAppOperation)
|
||||
progress.addChild(installAppProgress, withPendingUnitCount: 55)
|
||||
|
||||
group.add([installBackupAppOperation, backupAppOperation, installAppOperation])
|
||||
self.run([installBackupAppOperation, installAppOperation, backupAppOperation], context: group.context)
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
private func _installBackupApp(for app: InstalledApp, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
{
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
guard let application = ALTApplication(fileURL: app.fileURL) else {
|
||||
completionHandler(.failure(OperationError.appNotFound))
|
||||
return progress
|
||||
}
|
||||
|
||||
let prepareProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
let prepareOperation = RSTAsyncBlockOperation { (operation) in
|
||||
app.managedObjectContext?.perform {
|
||||
do
|
||||
{
|
||||
let temporaryDirectoryURL = context.temporaryDirectory.appendingPathComponent("AltBackup-" + UUID().uuidString)
|
||||
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
guard let altbackupFileURL = Bundle.main.url(forResource: "AltBackup", withExtension: "ipa") else { throw OperationError.appNotFound }
|
||||
|
||||
let unzippedAppBundleURL = try FileManager.default.unzipAppBundle(at: altbackupFileURL, toDirectory: temporaryDirectoryURL)
|
||||
guard let unzippedAppBundle = Bundle(url: unzippedAppBundleURL) else { throw OperationError.invalidApp }
|
||||
|
||||
if var infoDictionary = unzippedAppBundle.infoDictionary
|
||||
{
|
||||
// Replace name + bundle identifier so AltStore treats it as the same app.
|
||||
infoDictionary["CFBundleDisplayName"] = app.name
|
||||
infoDictionary[kCFBundleIdentifierKey as String] = app.bundleIdentifier
|
||||
|
||||
// Add app-specific exported UTI so we can check later if this temporary backup app is still installed or not.
|
||||
let installedAppUTI = ["UTTypeConformsTo": [],
|
||||
"UTTypeDescription": "AltStore Backup App",
|
||||
"UTTypeIconFiles": [],
|
||||
"UTTypeIdentifier": app.installedBackupAppUTI,
|
||||
"UTTypeTagSpecification": [:]] as [String : Any]
|
||||
|
||||
var exportedUTIs = infoDictionary[Bundle.Info.exportedUTIs] as? [[String: Any]] ?? []
|
||||
exportedUTIs.append(installedAppUTI)
|
||||
infoDictionary[Bundle.Info.exportedUTIs] = exportedUTIs
|
||||
|
||||
if let cachedApp = ALTApplication(fileURL: app.fileURL), let icon = cachedApp.icon?.resizing(to: CGSize(width: 180, height: 180))
|
||||
{
|
||||
let iconFileURL = unzippedAppBundleURL.appendingPathComponent("AppIcon.png")
|
||||
|
||||
if let iconData = icon.pngData()
|
||||
{
|
||||
do
|
||||
{
|
||||
try iconData.write(to: iconFileURL, options: .atomic)
|
||||
|
||||
let bundleIcons = ["CFBundlePrimaryIcon": ["CFBundleIconFiles": [iconFileURL.lastPathComponent]]]
|
||||
infoDictionary["CFBundleIcons"] = bundleIcons
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to write app icon data.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try (infoDictionary as NSDictionary).write(to: unzippedAppBundle.infoPlistURL)
|
||||
}
|
||||
|
||||
guard let backupApp = ALTApplication(fileURL: unzippedAppBundleURL) else { throw OperationError.invalidApp }
|
||||
context.app = backupApp
|
||||
|
||||
prepareProgress.completedUnitCount += 1
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
}
|
||||
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
progress.addChild(prepareProgress, withPendingUnitCount: 20)
|
||||
|
||||
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let installOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||
guard let self = self else { return }
|
||||
|
||||
guard let backupApp = context.app else {
|
||||
context.error = OperationError.invalidApp
|
||||
operation.finish()
|
||||
return
|
||||
}
|
||||
|
||||
var appGroups = application.entitlements[.appGroups] as? [String] ?? []
|
||||
appGroups.append(Bundle.baseAltStoreAppGroupID)
|
||||
|
||||
let additionalEntitlements: [ALTEntitlement: Any] = [.appGroups: appGroups]
|
||||
let progress = self._install(backupApp, operation: appOperation, group: group, context: context, additionalEntitlements: additionalEntitlements, cacheApp: false) { (result) in
|
||||
completionHandler(result)
|
||||
operation.finish()
|
||||
}
|
||||
installProgress.addChild(progress, withPendingUnitCount: 100)
|
||||
}
|
||||
installOperation.addDependency(prepareOperation)
|
||||
progress.addChild(installProgress, withPendingUnitCount: 80)
|
||||
|
||||
group.add([prepareOperation, installOperation])
|
||||
self.run([prepareOperation, installOperation], context: group.context)
|
||||
|
||||
return progress
|
||||
}
|
||||
@@ -713,6 +1270,7 @@ private extension AppManager
|
||||
event = nil
|
||||
|
||||
case .update: event = .updatedApp(installedApp)
|
||||
case .activate, .deactivate, .backup, .restore: event = nil
|
||||
}
|
||||
|
||||
if let event = event
|
||||
@@ -757,17 +1315,8 @@ private extension AppManager
|
||||
switch operation
|
||||
{
|
||||
case _ where requiresSerialQueue: fallthrough
|
||||
case is InstallAppOperation, is RefreshAppOperation:
|
||||
if let context = context, let previousOperation = self.serialOperationQueue.operations.last(where: { context.operations.contains($0) })
|
||||
{
|
||||
// Ensure operations execute in the order they're added (in same context), since they may become ready at different points.
|
||||
operation.addDependency(previousOperation)
|
||||
}
|
||||
|
||||
self.serialOperationQueue.addOperation(operation)
|
||||
|
||||
default:
|
||||
self.operationQueue.addOperation(operation)
|
||||
case is InstallAppOperation, is RefreshAppOperation, is BackupAppOperation: self.serialOperationQueue.addOperation(operation)
|
||||
default: self.operationQueue.addOperation(operation)
|
||||
}
|
||||
|
||||
context?.operations.add(operation)
|
||||
@@ -779,7 +1328,7 @@ private extension AppManager
|
||||
switch operation
|
||||
{
|
||||
case .install, .update: return self.installationProgress[operation.bundleIdentifier]
|
||||
case .refresh: return self.refreshProgress[operation.bundleIdentifier]
|
||||
case .refresh, .activate, .deactivate, .backup, .restore: return self.refreshProgress[operation.bundleIdentifier]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -788,7 +1337,7 @@ private extension AppManager
|
||||
switch operation
|
||||
{
|
||||
case .install, .update: self.installationProgress[operation.bundleIdentifier] = progress
|
||||
case .refresh: self.refreshProgress[operation.bundleIdentifier] = progress
|
||||
case .refresh, .activate, .deactivate, .backup, .restore: self.refreshProgress[operation.bundleIdentifier] = progress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,11 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
|
||||
return 1 + self.appExtensions.count
|
||||
}
|
||||
|
||||
var requiredActiveSlots: Int {
|
||||
let requiredActiveSlots = UserDefaults.standard.activeAppLimitIncludesExtensions ? self.appIDCount : 1
|
||||
return requiredActiveSlots
|
||||
}
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
@@ -247,6 +252,12 @@ extension InstalledApp
|
||||
return installedAppUTI
|
||||
}
|
||||
|
||||
class func installedBackupAppUTI(forBundleIdentifier bundleIdentifier: String) -> String
|
||||
{
|
||||
let installedBackupAppUTI = InstalledApp.installedAppUTI(forBundleIdentifier: bundleIdentifier) + ".backup"
|
||||
return installedBackupAppUTI
|
||||
}
|
||||
|
||||
var directoryURL: URL {
|
||||
return InstalledApp.directoryURL(for: self)
|
||||
}
|
||||
@@ -262,4 +273,8 @@ extension InstalledApp
|
||||
var installedAppUTI: String {
|
||||
return InstalledApp.installedAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
||||
}
|
||||
|
||||
var installedBackupAppUTI: String {
|
||||
return InstalledApp.installedBackupAppUTI(forBundleIdentifier: self.resignedBundleIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ extension MyAppsViewController
|
||||
|
||||
class MyAppsViewController: UICollectionViewController
|
||||
{
|
||||
private let coordinator = NSFileCoordinator()
|
||||
private let operationQueue = OperationQueue()
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var noUpdatesDataSource = self.makeNoUpdatesDataSource()
|
||||
private lazy var updatesDataSource = self.makeUpdatesDataSource()
|
||||
@@ -404,6 +407,15 @@ private extension MyAppsViewController
|
||||
|
||||
// Ensure no leftover progress from active apps cell reuse.
|
||||
cell.bannerView.button.progress = nil
|
||||
|
||||
if let progress = AppManager.shared.refreshProgress(for: installedApp), progress.fractionCompleted < 1.0
|
||||
{
|
||||
cell.bannerView.button.progress = progress
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.bannerView.button.progress = nil
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
||||
let fileURL = item.fileURL
|
||||
@@ -683,14 +695,157 @@ private extension MyAppsViewController
|
||||
self.present(documentPickerViewController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func sideloadApp(at fileURL: URL, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func sideloadApp(at url: URL, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
let progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true
|
||||
|
||||
func finish(_ result: Result<ALTApplication, Error>)
|
||||
class Context
|
||||
{
|
||||
var fileURL: URL?
|
||||
var application: ALTApplication?
|
||||
var installedApp: InstalledApp? {
|
||||
didSet {
|
||||
self.installedAppContext = self.installedApp?.managedObjectContext
|
||||
}
|
||||
}
|
||||
private var installedAppContext: NSManagedObjectContext?
|
||||
|
||||
var error: Error?
|
||||
}
|
||||
|
||||
let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
let unzippedAppDirectory = temporaryDirectory.appendingPathComponent("App")
|
||||
|
||||
let context = Context()
|
||||
|
||||
let downloadOperation: RSTAsyncBlockOperation?
|
||||
|
||||
if url.isFileURL
|
||||
{
|
||||
downloadOperation = nil
|
||||
context.fileURL = url
|
||||
progress.totalUnitCount -= 20
|
||||
}
|
||||
else
|
||||
{
|
||||
let downloadProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
downloadOperation = RSTAsyncBlockOperation { (operation) in
|
||||
let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in
|
||||
do
|
||||
{
|
||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||
|
||||
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let destinationURL = temporaryDirectory.appendingPathComponent("App.ipa")
|
||||
try FileManager.default.moveItem(at: fileURL, to: destinationURL)
|
||||
|
||||
context.fileURL = destinationURL
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.error = error
|
||||
}
|
||||
operation.finish()
|
||||
}
|
||||
downloadProgress.addChild(downloadTask.progress, withPendingUnitCount: 100)
|
||||
downloadTask.resume()
|
||||
}
|
||||
progress.addChild(downloadProgress, withPendingUnitCount: 20)
|
||||
}
|
||||
|
||||
let unzipProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
let unzipAppOperation = BlockOperation {
|
||||
do
|
||||
{
|
||||
if let error = context.error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let fileURL = context.fileURL else { throw OperationError.invalidParameters }
|
||||
|
||||
try FileManager.default.createDirectory(at: unzippedAppDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: unzippedAppDirectory)
|
||||
|
||||
guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { throw OperationError.invalidApp }
|
||||
context.application = application
|
||||
|
||||
unzipProgress.completedUnitCount = 1
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.error = error
|
||||
}
|
||||
}
|
||||
progress.addChild(unzipProgress, withPendingUnitCount: 10)
|
||||
|
||||
if let downloadOperation = downloadOperation
|
||||
{
|
||||
unzipAppOperation.addDependency(downloadOperation)
|
||||
}
|
||||
|
||||
let removeAppExtensionsProgress = Progress.discreteProgress(totalUnitCount: 1)
|
||||
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||
do
|
||||
{
|
||||
if let error = context.error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let application = context.application else { throw OperationError.invalidParameters }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self?.removeAppExtensions(from: application) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: removeAppExtensionsProgress.completedUnitCount = 1
|
||||
case .failure(let error): context.error = error
|
||||
}
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.error = error
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
removeAppExtensionsOperation.addDependency(unzipAppOperation)
|
||||
progress.addChild(removeAppExtensionsProgress, withPendingUnitCount: 5)
|
||||
|
||||
let installProgress = Progress.discreteProgress(totalUnitCount: 100)
|
||||
let installAppOperation = RSTAsyncBlockOperation { (operation) in
|
||||
do
|
||||
{
|
||||
if let error = context.error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let application = context.application else { throw OperationError.invalidParameters }
|
||||
|
||||
let progress = AppManager.shared.install(application, presentingViewController: self) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success(let installedApp): context.installedApp = installedApp
|
||||
case .failure(let error): context.error = error
|
||||
}
|
||||
operation.finish()
|
||||
}
|
||||
installProgress.addChild(progress, withPendingUnitCount: 100)
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.error = error
|
||||
operation.finish()
|
||||
}
|
||||
}
|
||||
installAppOperation.completionBlock = {
|
||||
try? FileManager.default.removeItem(at: temporaryDirectory)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
@@ -698,13 +853,17 @@ private extension MyAppsViewController
|
||||
self.sideloadingProgressView.observedProgress = nil
|
||||
self.sideloadingProgressView.setHidden(true, animated: true)
|
||||
|
||||
switch result
|
||||
switch Result(context.installedApp, context.error)
|
||||
{
|
||||
case .success(let app):
|
||||
print("Successfully installed app:", app.bundleIdentifier)
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(OperationError.cancelled): break
|
||||
app.managedObjectContext?.perform {
|
||||
print("Successfully installed app:", app.bundleIdentifier)
|
||||
}
|
||||
|
||||
case .failure(OperationError.cancelled):
|
||||
completion(.failure((OperationError.cancelled)))
|
||||
|
||||
case .failure(let error):
|
||||
let toastView = ToastView(error: error)
|
||||
@@ -714,68 +873,16 @@ private extension MyAppsViewController
|
||||
}
|
||||
}
|
||||
}
|
||||
progress.addChild(installProgress, withPendingUnitCount: 65)
|
||||
installAppOperation.addDependency(removeAppExtensionsOperation)
|
||||
|
||||
DispatchQueue.global().async {
|
||||
do
|
||||
{
|
||||
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory)
|
||||
|
||||
guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { throw OperationError.invalidApp }
|
||||
|
||||
func install()
|
||||
{
|
||||
self.sideloadingProgress = AppManager.shared.install(application, presentingViewController: self) { (result) in
|
||||
finish(result.map { _ in application })
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.sideloadingProgressView.progress = 0
|
||||
self.sideloadingProgressView.isHidden = false
|
||||
self.sideloadingProgressView.observedProgress = self.sideloadingProgress
|
||||
}
|
||||
}
|
||||
|
||||
if !application.appExtensions.isEmpty
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions. Would you like to remove this app's app extensions so they don't count towards your limit?", comment: ""), preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
|
||||
finish(.failure(OperationError.cancelled))
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
|
||||
install()
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
||||
do
|
||||
{
|
||||
for appExtension in application.appExtensions
|
||||
{
|
||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
||||
}
|
||||
|
||||
install()
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
})
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
install()
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
}
|
||||
self.sideloadingProgress = progress
|
||||
self.sideloadingProgressView.progress = 0
|
||||
self.sideloadingProgressView.isHidden = false
|
||||
self.sideloadingProgressView.observedProgress = self.sideloadingProgress
|
||||
|
||||
let operations = [downloadOperation, unzipAppOperation, removeAppExtensionsOperation, installAppOperation].compactMap { $0 }
|
||||
self.operationQueue.addOperations(operations, waitUntilFinished: false)
|
||||
}
|
||||
|
||||
@IBAction func activateApp(_ sender: UIButton)
|
||||
@@ -798,7 +905,18 @@ private extension MyAppsViewController
|
||||
|
||||
@objc func presentInactiveAppsAlert()
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("What are inactive apps?", comment: ""), message: NSLocalizedString("Free developer accounts are limited to 3 apps and app extensions. Inactive apps don't count towards your total, but cannot be opened until activated.", comment: ""), preferredStyle: .alert)
|
||||
let message: String
|
||||
|
||||
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
||||
{
|
||||
message = NSLocalizedString("Free developer accounts are limited to 3 apps and app extensions. Inactive apps don't count towards your total, but cannot be opened until activated.", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
message = NSLocalizedString("Free developer accounts are limited to 3 apps. Inactive apps are backed up and uninstalled so they don't count towards your total, but will be reinstalled with all their data when activated again.", comment: "")
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("What are inactive apps?", comment: ""), message: message, preferredStyle: .alert)
|
||||
alertController.addAction(.ok)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
@@ -812,6 +930,49 @@ private extension MyAppsViewController
|
||||
|
||||
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||
}
|
||||
|
||||
func removeAppExtensions(from application: ALTApplication, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
|
||||
|
||||
let firstSentence: String
|
||||
|
||||
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
||||
{
|
||||
firstSentence = NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions.", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
firstSentence = NSLocalizedString("Free developer accounts are limited to creating 10 App IDs per week.", comment: "")
|
||||
}
|
||||
|
||||
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit?", comment: "")
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
|
||||
completion(.failure(OperationError.cancelled))
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
|
||||
completion(.success(()))
|
||||
})
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
|
||||
do
|
||||
{
|
||||
for appExtension in application.appExtensions
|
||||
{
|
||||
try FileManager.default.removeItem(at: appExtension.fileURL)
|
||||
}
|
||||
|
||||
completion(.success(()))
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
})
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private extension MyAppsViewController
|
||||
@@ -887,7 +1048,7 @@ private extension MyAppsViewController
|
||||
guard installedApp.isActive else { return }
|
||||
installedApp.isActive = false
|
||||
|
||||
AppManager.shared.deactivate(installedApp) { (result) in
|
||||
AppManager.shared.deactivate(installedApp, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
@@ -918,12 +1079,34 @@ private extension MyAppsViewController
|
||||
let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext)
|
||||
.filter { $0.bundleIdentifier != installedApp.bundleIdentifier } // Don't count app towards total if it matches activating app
|
||||
|
||||
let activeAppsCount = activeApps.map { $0.appIDCount }.reduce(0, +)
|
||||
var title: String = NSLocalizedString("Cannot Activate More than 3 Apps", comment: "")
|
||||
let message: String
|
||||
|
||||
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
||||
{
|
||||
if installedApp.appExtensions.isEmpty
|
||||
{
|
||||
message = NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions. Please choose an app to deactivate.", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
title = NSLocalizedString("Cannot Activate More than 3 Apps and App Extensions", comment: "")
|
||||
|
||||
let appExtensionText = installedApp.appExtensions.count == 1 ? NSLocalizedString("app extension", comment: "") : NSLocalizedString("app extensions", comment: "")
|
||||
message = String(format: NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions, and “%@” contains %@ %@. Please choose an app to deactivate.", comment: ""), installedApp.name, NSNumber(value: installedApp.appExtensions.count), appExtensionText)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
message = NSLocalizedString("Free developer accounts are limited to 3 active apps. Please choose an app to deactivate.", comment: "")
|
||||
}
|
||||
|
||||
let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +)
|
||||
|
||||
let availableActiveApps = max(activeAppsLimit - activeAppsCount, 0)
|
||||
guard installedApp.appIDCount > availableActiveApps else { return completion(true) }
|
||||
guard installedApp.requiredActiveSlots > availableActiveApps else { return completion(true) }
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Cannot Activate More than 3 Apps", comment: ""), message: NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions. Please choose an app to deactivate.", comment: ""), preferredStyle: .alert)
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { (action) in
|
||||
completion(false)
|
||||
})
|
||||
@@ -931,8 +1114,8 @@ private extension MyAppsViewController
|
||||
for app in activeApps where app.bundleIdentifier != StoreApp.altstoreAppID
|
||||
{
|
||||
alertController.addAction(UIAlertAction(title: app.name, style: .default) { (action) in
|
||||
let availableActiveApps = availableActiveApps + app.appIDCount
|
||||
if availableActiveApps >= installedApp.appIDCount
|
||||
let availableActiveApps = availableActiveApps + app.requiredActiveSlots
|
||||
if availableActiveApps >= installedApp.requiredActiveSlots
|
||||
{
|
||||
// There are enough slots now to activate the app, so pre-emptively
|
||||
// mark it as active to provide visual feedback sooner.
|
||||
@@ -960,20 +1143,117 @@ private extension MyAppsViewController
|
||||
|
||||
func remove(_ installedApp: InstalledApp)
|
||||
{
|
||||
let alertController = UIAlertController(title: nil, message: NSLocalizedString("Removing a sideloaded app only removes it from AltStore. You must also delete it from the home screen to fully uninstall the app.", comment: ""), preferredStyle: .actionSheet)
|
||||
let title = String(format: NSLocalizedString("Remove “%@” from AltStore?", comment: ""), installedApp.name)
|
||||
let message: String
|
||||
|
||||
if UserDefaults.standard.isLegacyDeactivationSupported
|
||||
{
|
||||
message = NSLocalizedString("You must also delete it from the home screen to fully uninstall the app.", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
message = NSLocalizedString("This will also erase all backup data for this app.", comment: "")
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let installedApp = context.object(with: installedApp.objectID) as! InstalledApp
|
||||
context.delete(installedApp)
|
||||
|
||||
do { try context.save() }
|
||||
catch { print("Failed to remove sideloaded app.", error) }
|
||||
AppManager.shared.remove(installedApp) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: break
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func backup(_ installedApp: InstalledApp)
|
||||
{
|
||||
let title = NSLocalizedString("Start Backup?", comment: "")
|
||||
let message = NSLocalizedString("This will replace any previous backups. Please leave AltStore open until the backup is complete.", comment: "")
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
|
||||
let actionTitle = String(format: NSLocalizedString("Back Up %@", comment: ""), installedApp.name)
|
||||
alertController.addAction(UIAlertAction(title: actionTitle, style: .default, handler: { (action) in
|
||||
AppManager.shared.backup(installedApp, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
try? app.managedObjectContext?.save()
|
||||
|
||||
print("Finished backing up app:", app.bundleIdentifier)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to back up app:", error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
|
||||
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||
}
|
||||
}))
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func restore(_ installedApp: InstalledApp)
|
||||
{
|
||||
let message = String(format: NSLocalizedString("This will replace all data you currently have in %@.", comment: ""), installedApp.name)
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to restore this backup?", comment: ""), message: message, preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Restore Backup", comment: ""), style: .destructive, handler: { (action) in
|
||||
AppManager.shared.restore(installedApp, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
try? app.managedObjectContext?.save()
|
||||
|
||||
print("Finished restoring app:", app.bundleIdentifier)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to restore app:", error)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.collectionView.reloadSections([Section.activeApps.rawValue])
|
||||
}
|
||||
}))
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func exportBackup(for installedApp: InstalledApp)
|
||||
{
|
||||
guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return }
|
||||
|
||||
let documentPicker = UIDocumentPickerViewController(url: backupURL, in: .exportToService)
|
||||
documentPicker.delegate = self
|
||||
self.present(documentPicker, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private extension MyAppsViewController
|
||||
@@ -996,12 +1276,14 @@ private extension MyAppsViewController
|
||||
// Make sure left UIBarButtonItem has been set.
|
||||
self.loadViewIfNeeded()
|
||||
|
||||
guard let fileURL = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return }
|
||||
guard let url = notification.userInfo?[AppDelegate.importAppDeepLinkURLKey] as? URL else { return }
|
||||
|
||||
self.sideloadApp(at: fileURL) { (result) in
|
||||
self.sideloadApp(at: url) { (result) in
|
||||
guard url.isFileURL else { return }
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: fileURL)
|
||||
try FileManager.default.removeItem(at: url)
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -1161,39 +1443,100 @@ extension MyAppsViewController
|
||||
self.remove(installedApp)
|
||||
}
|
||||
|
||||
if installedApp.bundleIdentifier == StoreApp.altstoreAppID
|
||||
let backupAction = UIAction(title: NSLocalizedString("Back Up", comment: ""), image: UIImage(systemName: "doc.on.doc")) { (action) in
|
||||
self.backup(installedApp)
|
||||
}
|
||||
|
||||
let exportBackupAction = UIAction(title: NSLocalizedString("Export Backup", comment: ""), image: UIImage(systemName: "arrow.up.doc")) { (action) in
|
||||
self.exportBackup(for: installedApp)
|
||||
}
|
||||
|
||||
let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in
|
||||
self.restore(installedApp)
|
||||
}
|
||||
|
||||
guard installedApp.bundleIdentifier != StoreApp.altstoreAppID else {
|
||||
return [refreshAction]
|
||||
}
|
||||
|
||||
if installedApp.isActive
|
||||
{
|
||||
actions = [refreshAction]
|
||||
actions.append(refreshAction)
|
||||
}
|
||||
else
|
||||
{
|
||||
if installedApp.isActive
|
||||
{
|
||||
if UserDefaults.standard.activeAppsLimit != nil
|
||||
{
|
||||
actions = [refreshAction, deactivateAction]
|
||||
}
|
||||
else
|
||||
{
|
||||
actions = [refreshAction]
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
actions.append(activateAction)
|
||||
actions.append(activateAction)
|
||||
}
|
||||
|
||||
if installedApp.isActive
|
||||
{
|
||||
actions.append(backupAction)
|
||||
}
|
||||
else if let _ = UTTypeCopyDeclaration(installedApp.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?, !UserDefaults.standard.isLegacyDeactivationSupported
|
||||
{
|
||||
// Allow backing up inactive apps if they are still installed,
|
||||
// but on an iOS version that no longer supports legacy deactivation.
|
||||
// This handles edge case where you can't install more apps until you
|
||||
// delete some, but can't activate inactive apps again to back them up first.
|
||||
actions.append(backupAction)
|
||||
}
|
||||
|
||||
if let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp)
|
||||
{
|
||||
var backupExists = false
|
||||
var outError: NSError? = nil
|
||||
|
||||
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
|
||||
#if DEBUG
|
||||
backupExists = true
|
||||
#else
|
||||
backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
actions.append(removeAction)
|
||||
#else
|
||||
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
|
||||
if backupExists
|
||||
{
|
||||
// Only display option for legacy sideloaded apps.
|
||||
actions.append(removeAction)
|
||||
actions.append(exportBackupAction)
|
||||
|
||||
if installedApp.isActive
|
||||
{
|
||||
actions.append(restoreBackupAction)
|
||||
}
|
||||
}
|
||||
else if let error = outError
|
||||
{
|
||||
print("Unable to check if backup exists:", error)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if installedApp.isActive
|
||||
{
|
||||
actions.append(deactivateAction)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
if installedApp.bundleIdentifier != StoreApp.altstoreAppID
|
||||
{
|
||||
actions.append(removeAction)
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
|
||||
{
|
||||
// Legacy sideloaded app, so can't detect if it's deleted.
|
||||
actions.append(removeAction)
|
||||
}
|
||||
else if !UserDefaults.standard.isLegacyDeactivationSupported && !installedApp.isActive
|
||||
{
|
||||
// Inactive apps are actually deleted, so we need another way
|
||||
// for user to remove them from AltStore.
|
||||
actions.append(removeAction)
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
@@ -1463,10 +1806,10 @@ extension MyAppsViewController: UICollectionViewDropDelegate
|
||||
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
|
||||
}
|
||||
|
||||
let activeAppsCount = (self.activeAppsDataSource.fetchedResultsController.fetchedObjects ?? []).map { $0.appIDCount }.reduce(0, +)
|
||||
let activeAppsCount = (self.activeAppsDataSource.fetchedResultsController.fetchedObjects ?? []).map { $0.requiredActiveSlots }.reduce(0, +)
|
||||
let availableActiveApps = max(activeAppsLimit - activeAppsCount, 0)
|
||||
|
||||
if installedApp.appIDCount <= availableActiveApps
|
||||
if installedApp.requiredActiveSlots <= availableActiveApps
|
||||
{
|
||||
// Enough active app slots, so no need to deactivate app first.
|
||||
return UICollectionViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
|
||||
@@ -1586,8 +1929,15 @@ extension MyAppsViewController: UIDocumentPickerDelegate
|
||||
{
|
||||
guard let fileURL = urls.first else { return }
|
||||
|
||||
self.sideloadApp(at: fileURL) { (result) in
|
||||
print("Sideloaded app at \(fileURL) with result:", result)
|
||||
switch controller.documentPickerMode
|
||||
{
|
||||
case .import, .open:
|
||||
self.sideloadApp(at: fileURL) { (result) in
|
||||
print("Sideloaded app at \(fileURL) with result:", result)
|
||||
}
|
||||
|
||||
case .exportToService, .moveToService: break
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
183
AltStore/Operations/BackupAppOperation.swift
Normal file
183
AltStore/Operations/BackupAppOperation.swift
Normal file
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// BackupAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/12/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltKit
|
||||
import AltSign
|
||||
|
||||
extension BackupAppOperation
|
||||
{
|
||||
enum Action: String
|
||||
{
|
||||
case backup
|
||||
case restore
|
||||
}
|
||||
}
|
||||
|
||||
@objc(BackupAppOperation)
|
||||
class BackupAppOperation: ResultOperation<Void>
|
||||
{
|
||||
let action: Action
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
private var appName: String?
|
||||
private var timeoutTimer: Timer?
|
||||
|
||||
init(action: Action, context: InstallAppOperationContext)
|
||||
{
|
||||
self.action = action
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
do
|
||||
{
|
||||
if let error = self.context.error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
guard let installedApp = self.context.installedApp, let context = installedApp.managedObjectContext else { throw OperationError.invalidParameters }
|
||||
context.perform {
|
||||
do
|
||||
{
|
||||
let appName = installedApp.name
|
||||
self.appName = appName
|
||||
|
||||
guard let altstoreApp = InstalledApp.fetchAltStore(in: context) else { throw OperationError.appNotFound }
|
||||
let altstoreOpenURL = altstoreApp.openAppURL
|
||||
|
||||
var returnURLComponents = URLComponents(url: altstoreOpenURL, resolvingAgainstBaseURL: false)
|
||||
returnURLComponents?.host = "appBackupResponse"
|
||||
guard let returnURL = returnURLComponents?.url else { throw OperationError.openAppFailed(name: appName) }
|
||||
|
||||
var openURLComponents = URLComponents()
|
||||
openURLComponents.scheme = installedApp.openAppURL.scheme
|
||||
openURLComponents.host = self.action.rawValue
|
||||
openURLComponents.queryItems = [URLQueryItem(name: "returnURL", value: returnURL.absoluteString)]
|
||||
|
||||
guard let openURL = openURLComponents.url else { throw OperationError.openAppFailed(name: appName) }
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
let currentTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
UIApplication.shared.open(openURL, options: [:]) { (success) in
|
||||
let elapsedTime = CFAbsoluteTimeGetCurrent() - currentTime
|
||||
|
||||
if success
|
||||
{
|
||||
self.registerObservers()
|
||||
}
|
||||
else if elapsedTime < 0.5
|
||||
{
|
||||
// Failed too quickly for human to respond to alert, possibly still finalizing installation.
|
||||
// Try again in a couple seconds.
|
||||
|
||||
print("Failed too quickly, retrying after a few seconds...")
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
UIApplication.shared.open(openURL, options: [:]) { (success) in
|
||||
if success
|
||||
{
|
||||
self.registerObservers()
|
||||
}
|
||||
else
|
||||
{
|
||||
self.finish(.failure(OperationError.openAppFailed(name: appName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.finish(.failure(OperationError.openAppFailed(name: appName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
override func finish(_ result: Result<Void, Error>)
|
||||
{
|
||||
let result = result.mapError { (error) -> Error in
|
||||
let appName = self.appName ?? self.context.bundleIdentifier
|
||||
|
||||
switch (error, self.action)
|
||||
{
|
||||
case (let error as NSError, _) where (self.context.error as NSError?) == error: fallthrough
|
||||
case (OperationError.cancelled, _):
|
||||
return error
|
||||
|
||||
case (let error as NSError, .backup):
|
||||
let localizedFailure = String(format: NSLocalizedString("Could not back up “%@”.", comment: ""), appName)
|
||||
return error.withLocalizedFailure(localizedFailure)
|
||||
|
||||
case (let error as NSError, .restore):
|
||||
let localizedFailure = String(format: NSLocalizedString("Could not restore “%@”.", comment: ""), appName)
|
||||
return error.withLocalizedFailure(localizedFailure)
|
||||
}
|
||||
}
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success: self.progress.completedUnitCount += 1
|
||||
case .failure: break
|
||||
}
|
||||
|
||||
super.finish(result)
|
||||
}
|
||||
}
|
||||
|
||||
private extension BackupAppOperation
|
||||
{
|
||||
func registerObservers()
|
||||
{
|
||||
var applicationWillReturnObserver: NSObjectProtocol!
|
||||
applicationWillReturnObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] (notification) in
|
||||
guard let self = self, !self.isFinished else { return }
|
||||
|
||||
self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] (timer) in
|
||||
// Final delay to ensure we don't prematurely return failure
|
||||
// in case timer expired while we were in background, but
|
||||
// are now returning to app with success response.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
guard let self = self, !self.isFinished else { return }
|
||||
self.finish(.failure(OperationError.timedOut))
|
||||
}
|
||||
}
|
||||
|
||||
NotificationCenter.default.removeObserver(applicationWillReturnObserver!)
|
||||
}
|
||||
|
||||
var backupResponseObserver: NSObjectProtocol!
|
||||
backupResponseObserver = NotificationCenter.default.addObserver(forName: AppDelegate.appBackupDidFinish, object: nil, queue: nil) { [weak self] (notification) in
|
||||
self?.timeoutTimer?.invalidate()
|
||||
|
||||
let result = notification.userInfo?[AppDelegate.appBackupResultKey] as? Result<Void, Error> ?? .failure(OperationError.unknownResult)
|
||||
self?.finish(result)
|
||||
|
||||
NotificationCenter.default.removeObserver(backupResponseObserver!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,14 +23,14 @@ class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||
|
||||
private let session = URLSession(configuration: .default)
|
||||
|
||||
init(app: AppProtocol, context: AppOperationContext)
|
||||
init(app: AppProtocol, destinationURL: URL, context: AppOperationContext)
|
||||
{
|
||||
self.app = app
|
||||
self.context = context
|
||||
|
||||
self.bundleIdentifier = app.bundleIdentifier
|
||||
self.sourceURL = app.url
|
||||
self.destinationURL = InstalledApp.fileURL(for: app)
|
||||
self.destinationURL = destinationURL
|
||||
|
||||
super.init()
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioni
|
||||
{
|
||||
let context: AppOperationContext
|
||||
|
||||
var additionalEntitlements: [ALTEntitlement: Any]?
|
||||
|
||||
private let appGroupsLock = NSLock()
|
||||
|
||||
init(context: AppOperationContext)
|
||||
@@ -300,14 +302,20 @@ extension FetchProvisioningProfilesOperation
|
||||
|
||||
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
let requiredFeatures = app.entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
|
||||
var entitlements = app.entitlements
|
||||
for (key, value) in additionalEntitlements ?? [:]
|
||||
{
|
||||
entitlements[key] = value
|
||||
}
|
||||
|
||||
let requiredFeatures = entitlements.compactMap { (entitlement, value) -> (ALTFeature, Any)? in
|
||||
guard let feature = ALTFeature(entitlement: entitlement) else { return nil }
|
||||
return (feature, value)
|
||||
}
|
||||
|
||||
var features = requiredFeatures.reduce(into: [ALTFeature: Any]()) { $0[$1.0] = $1.1 }
|
||||
|
||||
if let applicationGroups = app.entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
|
||||
if let applicationGroups = entitlements[.appGroups] as? [String], !applicationGroups.isEmpty
|
||||
{
|
||||
features[.appGroups] = true
|
||||
}
|
||||
@@ -348,56 +356,101 @@ extension FetchProvisioningProfilesOperation
|
||||
|
||||
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
|
||||
{
|
||||
// TODO: Handle apps belonging to more than one app group.
|
||||
guard let applicationGroups = app.entitlements[.appGroups] as? [String], let groupIdentifier = applicationGroups.first else {
|
||||
return completionHandler(.success(appID))
|
||||
var entitlements = app.entitlements
|
||||
for (key, value) in additionalEntitlements ?? [:]
|
||||
{
|
||||
entitlements[key] = value
|
||||
}
|
||||
|
||||
var applicationGroups = entitlements[.appGroups] as? [String] ?? []
|
||||
if applicationGroups.isEmpty
|
||||
{
|
||||
guard let isAppGroupsEnabled = appID.features[.appGroups] as? Bool, isAppGroupsEnabled else {
|
||||
// No app groups, and we also haven't enabled the feature, so don't continue.
|
||||
// For apps with no app groups but have had the feature enabled already
|
||||
// we'll continue and assign the app ID to an empty array
|
||||
// in case we need to explicitly remove them.
|
||||
return completionHandler(.success(appID))
|
||||
}
|
||||
}
|
||||
|
||||
func finish(_ result: Result<ALTAppGroup, Error>)
|
||||
if app.bundleIdentifier == StoreApp.altstoreAppID
|
||||
{
|
||||
switch result
|
||||
// Updating app groups for this specific AltStore.
|
||||
// Find the (unique) AltStore app group, then replace it
|
||||
// with the correct "base" app group ID.
|
||||
// Otherwise, we may append a duplicate team identifier to the end.
|
||||
if let index = applicationGroups.firstIndex(where: { $0.contains(Bundle.baseAltStoreAppGroupID) })
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success(let group):
|
||||
// Assign App Group
|
||||
// TODO: Determine whether app already belongs to app group.
|
||||
|
||||
ALTAppleAPI.shared.add(appID, to: group, team: team, session: session) { (success, error) in
|
||||
let result = result.map { _ in appID }
|
||||
completionHandler(result)
|
||||
}
|
||||
applicationGroups[index] = Bundle.baseAltStoreAppGroupID
|
||||
}
|
||||
else
|
||||
{
|
||||
applicationGroups.append(Bundle.baseAltStoreAppGroupID)
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch onto global queue to prevent appGroupsLock deadlock.
|
||||
DispatchQueue.global().async {
|
||||
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
|
||||
|
||||
// Ensure we're not concurrently fetching and updating app groups,
|
||||
// which can lead to race conditions such as adding an app group twice.
|
||||
self.appGroupsLock.lock()
|
||||
|
||||
func finish(_ result: Result<ALTAppID, Error>)
|
||||
{
|
||||
self.appGroupsLock.unlock()
|
||||
completionHandler(result)
|
||||
}
|
||||
|
||||
ALTAppleAPI.shared.fetchAppGroups(for: team, session: session) { (groups, error) in
|
||||
switch Result(groups, error)
|
||||
{
|
||||
case .failure(let error):
|
||||
self.appGroupsLock.unlock()
|
||||
completionHandler(.failure(error))
|
||||
case .failure(let error): finish(.failure(error))
|
||||
case .success(let fetchedGroups):
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
case .success(let groups):
|
||||
if let group = groups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
|
||||
var groups = [ALTAppGroup]()
|
||||
var errors = [Error]()
|
||||
|
||||
for groupIdentifier in applicationGroups
|
||||
{
|
||||
self.appGroupsLock.unlock()
|
||||
finish(.success(group))
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
|
||||
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
|
||||
let adjustedGroupIdentifier = groupIdentifier + "." + team.identifier
|
||||
|
||||
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
|
||||
self.appGroupsLock.unlock()
|
||||
finish(Result(group, error))
|
||||
if let group = fetchedGroups.first(where: { $0.groupIdentifier == adjustedGroupIdentifier })
|
||||
{
|
||||
groups.append(group)
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
// Not all characters are allowed in group names, so we replace periods with spaces (like Apple does).
|
||||
let name = "AltStore " + groupIdentifier.replacingOccurrences(of: ".", with: " ")
|
||||
|
||||
ALTAppleAPI.shared.addAppGroup(withName: name, groupIdentifier: adjustedGroupIdentifier, team: team, session: session) { (group, error) in
|
||||
switch Result(group, error)
|
||||
{
|
||||
case .success(let group): groups.append(group)
|
||||
case .failure(let error): errors.append(error)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
if let error = errors.first
|
||||
{
|
||||
finish(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
ALTAppleAPI.shared.assign(appID, to: Array(groups), team: team, session: session) { (success, error) in
|
||||
let result = Result(success, error)
|
||||
finish(result.map { _ in appID })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,10 +122,10 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
||||
var activeApps = InstalledApp.fetch(fetchRequest, in: backgroundContext)
|
||||
if !activeApps.contains(installedApp)
|
||||
{
|
||||
let availableActiveApps = max(sideloadedAppsLimit - activeApps.count, 0)
|
||||
let requiredActiveAppSlots = 1 + installedExtensions.count // As of iOS 13.3.1, app extensions count as "apps"
|
||||
let activeAppsCount = activeApps.map { $0.requiredActiveSlots }.reduce(0, +)
|
||||
|
||||
if requiredActiveAppSlots <= availableActiveApps
|
||||
let availableActiveApps = max(sideloadedAppsLimit - activeAppsCount, 0)
|
||||
if installedApp.requiredActiveSlots <= availableActiveApps
|
||||
{
|
||||
// This app has not been explicitly activated, but there are enough slots available,
|
||||
// so implicitly activate it.
|
||||
|
||||
@@ -63,7 +63,7 @@ class AuthenticatedOperationContext: OperationContext
|
||||
class AppOperationContext
|
||||
{
|
||||
let bundleIdentifier: String
|
||||
private let authenticatedContext: AuthenticatedOperationContext
|
||||
let authenticatedContext: AuthenticatedOperationContext
|
||||
|
||||
var app: ALTApplication?
|
||||
var provisioningProfiles: [String: ALTProvisioningProfile]?
|
||||
@@ -105,6 +105,12 @@ class InstallAppOperationContext: AppOperationContext
|
||||
|
||||
var resignedApp: ALTApplication?
|
||||
var installationConnection: ServerConnection?
|
||||
var installedApp: InstalledApp? {
|
||||
didSet {
|
||||
self.installedAppContext = self.installedApp?.managedObjectContext
|
||||
}
|
||||
}
|
||||
private var installedAppContext: NSManagedObjectContext?
|
||||
|
||||
var beginInstallationHandler: ((InstalledApp) -> Void)?
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ enum OperationError: LocalizedError
|
||||
case unknown
|
||||
case unknownResult
|
||||
case cancelled
|
||||
case timedOut
|
||||
|
||||
case notAuthenticated
|
||||
case appNotFound
|
||||
@@ -28,17 +29,23 @@ enum OperationError: LocalizedError
|
||||
|
||||
case noSources
|
||||
|
||||
case openAppFailed(name: String)
|
||||
case missingAppGroup
|
||||
|
||||
var failureReason: String? {
|
||||
switch self {
|
||||
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
|
||||
case .unknownResult: return NSLocalizedString("The operation returned an unknown result.", comment: "")
|
||||
case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "")
|
||||
case .timedOut: return NSLocalizedString("The operation timed out.", comment: "")
|
||||
case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "")
|
||||
case .appNotFound: return NSLocalizedString("App not found.", comment: "")
|
||||
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
|
||||
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
|
||||
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
|
||||
case .noSources: return NSLocalizedString("There are no AltStore sources.", comment: "")
|
||||
case .openAppFailed(let name): return String(format: NSLocalizedString("AltStore was denied permission to launch %@.", comment: ""), name)
|
||||
case .missingAppGroup: return NSLocalizedString("AltStore's shared app group could not be found.", comment: "")
|
||||
case .iOSVersionNotSupported(let app):
|
||||
let name = app.name
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ class RefreshGroup: NSObject
|
||||
|
||||
private(set) var results = [String: Result<InstalledApp, Error>]()
|
||||
|
||||
// Keep strong references to managed object contexts
|
||||
// so they don't die out from under us.
|
||||
private(set) var _contexts = Set<NSManagedObjectContext>()
|
||||
|
||||
private var isFinished = false
|
||||
|
||||
private let dispatchGroup = DispatchGroup()
|
||||
@@ -33,6 +37,8 @@ class RefreshGroup: NSObject
|
||||
super.init()
|
||||
}
|
||||
|
||||
/// Used to keep track of which operations belong to this group.
|
||||
/// This does _not_ add them to any operation queue.
|
||||
func add(_ operations: [Foundation.Operation])
|
||||
{
|
||||
for operation in operations
|
||||
@@ -46,8 +52,8 @@ class RefreshGroup: NSObject
|
||||
|
||||
if self.operations.isEmpty && !operations.isEmpty
|
||||
{
|
||||
self.dispatchGroup.notify(queue: .global()) {
|
||||
self.finish()
|
||||
self.dispatchGroup.notify(queue: .global()) { [weak self] in
|
||||
self?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +63,14 @@ class RefreshGroup: NSObject
|
||||
func set(_ result: Result<InstalledApp, Error>, forAppWithBundleIdentifier bundleIdentifier: String)
|
||||
{
|
||||
self.results[bundleIdentifier] = result
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure: break
|
||||
case .success(let installedApp):
|
||||
guard let context = installedApp.managedObjectContext else { break }
|
||||
self._contexts.insert(context)
|
||||
}
|
||||
}
|
||||
|
||||
func cancel()
|
||||
|
||||
79
AltStore/Operations/RemoveAppBackupOperation.swift
Normal file
79
AltStore/Operations/RemoveAppBackupOperation.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// RemoveAppBackupOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/13/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltKit
|
||||
|
||||
@objc(RemoveAppBackupOperation)
|
||||
class RemoveAppBackupOperation: ResultOperation<Void>
|
||||
{
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
private let coordinator = NSFileCoordinator()
|
||||
private let coordinatorQueue = OperationQueue()
|
||||
|
||||
init(context: InstallAppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
|
||||
self.coordinatorQueue.name = "AltStore - RemoveAppBackupOperation Queue"
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
installedApp.managedObjectContext?.perform {
|
||||
guard let backupDirectoryURL = FileManager.default.backupDirectoryURL(for: installedApp) else { return self.finish(.failure(OperationError.missingAppGroup)) }
|
||||
|
||||
let intent = NSFileAccessIntent.writingIntent(with: backupDirectoryURL, options: [.forDeleting])
|
||||
self.coordinator.coordinate(with: [intent], queue: self.coordinatorQueue) { (error) in
|
||||
do
|
||||
{
|
||||
if let error = error
|
||||
{
|
||||
throw error
|
||||
}
|
||||
|
||||
try FileManager.default.removeItem(at: intent.url)
|
||||
|
||||
self.finish(.success(()))
|
||||
}
|
||||
catch let error as CocoaError where error.code == CocoaError.Code.fileNoSuchFile
|
||||
{
|
||||
#if DEBUG
|
||||
|
||||
// When debugging, it's expected that app groups don't match, so ignore.
|
||||
self.finish(.success(()))
|
||||
|
||||
#else
|
||||
|
||||
print("Failed to remove app backup directory:", error)
|
||||
self.finish(.failure(error))
|
||||
|
||||
#endif
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove app backup directory:", error)
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
AltStore/Operations/RemoveAppOperation.swift
Normal file
83
AltStore/Operations/RemoveAppOperation.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// RemoveAppOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 5/12/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import AltKit
|
||||
|
||||
@objc(RemoveAppOperation)
|
||||
class RemoveAppOperation: ResultOperation<InstalledApp>
|
||||
{
|
||||
let context: InstallAppOperationContext
|
||||
|
||||
init(context: InstallAppOperationContext)
|
||||
{
|
||||
self.context = context
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
if let error = self.context.error
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let server = self.context.server, let installedApp = self.context.installedApp else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return self.finish(.failure(OperationError.unknownUDID)) }
|
||||
|
||||
installedApp.managedObjectContext?.perform {
|
||||
let resignedBundleIdentifier = installedApp.resignedBundleIdentifier
|
||||
|
||||
ServerManager.shared.connect(to: server) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(let connection):
|
||||
print("Sending remove app request...")
|
||||
|
||||
let request = RemoveAppRequest(udid: udid, bundleIdentifier: resignedBundleIdentifier)
|
||||
connection.send(request) { (result) in
|
||||
print("Sent remove app request!")
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success:
|
||||
print("Waiting for remove app response...")
|
||||
connection.receiveResponse() { (result) in
|
||||
print("Receiving remove app response:", result)
|
||||
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): self.finish(.failure(error))
|
||||
case .success(.error(let response)): self.finish(.failure(response.error))
|
||||
case .success(.removeApp):
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
self.progress.completedUnitCount += 1
|
||||
|
||||
let installedApp = context.object(with: installedApp.objectID) as! InstalledApp
|
||||
installedApp.isActive = false
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
|
||||
case .success: self.finish(.failure(ALTServerError(.unknownResponse)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ private extension ResignAppOperation
|
||||
guard var infoDictionary = bundle.infoDictionary else { throw ALTError(.missingInfoPlist) }
|
||||
|
||||
infoDictionary[kCFBundleIdentifierKey as String] = profile.bundleIdentifier
|
||||
infoDictionary[Bundle.Info.altBundleID] = identifier
|
||||
|
||||
for (key, value) in additionalInfoDictionaryValues
|
||||
{
|
||||
|
||||
@@ -89,8 +89,13 @@ private extension SendAppOperation
|
||||
connection.send(appData, prependSize: false) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): completionHandler(.failure(error))
|
||||
case .success: completionHandler(.success(()))
|
||||
case .failure(let error):
|
||||
print("Failed to send app data (\(appData.count) bytes)")
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success:
|
||||
print("Successfully sent app data (\(appData.count) bytes)")
|
||||
completionHandler(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
AltStore/Resources/AltBackup.ipa
Normal file
BIN
AltStore/Resources/AltBackup.ipa
Normal file
Binary file not shown.
@@ -7,10 +7,10 @@
|
||||
"bundleIdentifier": "com.rileytestut.AltStore.Alpha",
|
||||
"developerName": "Riley Testut",
|
||||
"subtitle": "An alternative App Store for iOS.",
|
||||
"version": "1.3.3a1",
|
||||
"versionDate": "2020-05-09T12:00:00-07:00",
|
||||
"versionDescription": "NEW\n• Fixes \"App ID already registered\" error when updating DolphiniOS\n• Verifies app's bundle ID matches the source's when downloading from a source.",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/sources/alpha/altstore/1_3_3_a1.ipa",
|
||||
"version": "1.3.4a6",
|
||||
"versionDate": "2020-05-19T13:00:00-07:00",
|
||||
"versionDescription": "** Requires latest AltServer beta available for download in Discord **\n\nNEW:\n- Adds \"Back Up\" option to installed apps context menu\n\nPREVIOUS:\n- Removes active app extension limits on devices running iOS 13.5 or later\n- Correctly says \"backing up\" or \"restoring\" when opening temporary app during (de-)activation\n- Uses real app icon for temporary app during (de-)activation \n- Limits new (de-)activation method to iOS 13.5 or later\n- Fixes invalid entitlements when AltStore refreshes itself\n\nDue to iOS 13.5 changes, apps are now backed up & deleted when marked as inactive. When you later activate an inactive app, AltStore will reinstall the app, then restore your data from before.",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/sources/alpha/altstore/1_3_4_a6.ipa",
|
||||
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore allows you to install Delta as well as any app (.ipa) directly from the Files app.",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
|
||||
"tintColor": "018084",
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
"name": "AltStore",
|
||||
"bundleIdentifier": "com.rileytestut.AltStore",
|
||||
"developerName": "Riley Testut",
|
||||
"version": "1.3.3",
|
||||
"versionDate": "2020-05-09T12:00:00-07:00",
|
||||
"versionDescription": "NEW\n• Fixes \"App ID already registered\" and other errors when updating DolphiniOS\n\nPREVIOUS UPDATES:\n• Adds support for installing apps using private entitlements via Psychic Paper (https://siguza.github.io/psychicpaper/)\n• Displays \"Grant Permission\" alert that lists all private permissions when installing an app that uses them.\n• Sideload any app (.ipa file) directly from Files.\n• Manage which apps are (in-)active at any time from the My Apps tab (optionally via drag and drop)\n\nNOTE: No permission alert will appear for apps without private entitlements.",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/altstore/1_3_3.ipa",
|
||||
"version": "1.3.4",
|
||||
"versionDate": "2020-05-20T10:00:00-07:00",
|
||||
"versionDescription": "** iOS 13.5 Compatibility Update **\n\niOS 13.5 changes how active apps are counted. Previously, app extensions counted towards the 3 active app limit, and inactive apps could remain installed without counting. Now, app extensions are excluded, but inactive apps must be uninstalled to not count.\n\nOn devices running iOS 13.5 or later, apps will now be backed up & uninstalled when deactivated. When you later activate an inactive app, AltStore will reinstall the app and restore its data so you can continue using it as if it was never uninstalled.\n\nNOTE: This does not affect devices running iOS 13.4.1 or earlier.",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/altstore/1_3_4.ipa",
|
||||
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis version of AltStore allows you to install Delta, an all-in-one emulator for iOS, as well as sideload other .ipa files from the Files app.",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
|
||||
"tintColor": "018084",
|
||||
"size": 2541786,
|
||||
"size": 2665222,
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG",
|
||||
"https://user-images.githubusercontent.com/705880/78942222-0fe6da00-7a6e-11ea-9f2a-dda16157583c.PNG",
|
||||
@@ -36,14 +36,14 @@
|
||||
"bundleIdentifier": "com.rileytestut.AltStore.Beta",
|
||||
"developerName": "Riley Testut",
|
||||
"subtitle": "An alternative App Store for iOS.",
|
||||
"version": "1.3.3b1",
|
||||
"versionDate": "2020-05-08T12:00:00-07:00",
|
||||
"versionDescription": "NEW\n• Fixes \"App ID already registered\" error when updating DolphiniOS\n• Verifies app's bundle ID matches the source's when downloading from a source.\n\nPREVIOUS UPDATES\n• Adds support for installing apps using private entitlements via Psychic Paper (https://siguza.github.io/psychicpaper/)\n• Displays \"Grant Permission\" alert that lists all private entitlements when installing an app that uses them.\n\nNOTE: No permission alert will appear for apps without private entitlements.",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/altstore/1_3_3_b1.ipa",
|
||||
"version": "1.3.4b3",
|
||||
"versionDate": "2020-05-20T10:00:00-07:00",
|
||||
"versionDescription": "NEW\n• Adds \"Back Up\" option when long pressing app to manually back up app data, which can then be restored later.\n• GM version of 1.3.4\n\nPREVIOUS UPDATE:\n\n**Requires AltServer 1.3.1 beta. Download from https://altstore.io/altserver/beta/**\n\nIMPORTANT\niOS 13.5 (due for release soon) changes how active apps are counted. Previously, app extensions counted towards the 3 active app limit, and inactive apps could remain installed without counting. Now, app extensions are excluded, but inactive apps must be uninstalled to not count.\n\nOn devices running iOS 13.5 or later, apps will now be backed up & uninstalled when deactivated. When you later activate an inactive app, AltStore will reinstall the app and restore its data so you can continue using it as if it was never uninstalled.\n\nNOTE: This does not affect devices running iOS 13.4.1 or earlier.",
|
||||
"downloadURL": "https://f000.backblazeb2.com/file/altstore/apps/altstore/1_3_4_b3.ipa",
|
||||
"localizedDescription": "AltStore is an alternative app store for non-jailbroken devices. \n\nThis beta release of AltStore adds support for 3rd party sources, allowing you to download apps from other developers directly through AltStore.",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/65270980-1eb96f80-dad1-11e9-9367-78ccd25ceb02.png",
|
||||
"tintColor": "018084",
|
||||
"size": 2531143,
|
||||
"size": 2665126,
|
||||
"beta": true,
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/78942028-acf54300-7a6d-11ea-821c-5bb7a9b3e73a.PNG",
|
||||
|
||||
@@ -32,7 +32,7 @@ enum ConnectionError: LocalizedError
|
||||
case connectionFailed
|
||||
case connectionDropped
|
||||
|
||||
var errorDescription: String? {
|
||||
var failureReason: String? {
|
||||
switch self
|
||||
{
|
||||
case .serverNotFound: return NSLocalizedString("Could not find AltServer.", comment: "")
|
||||
|
||||
2
Dependencies/AltSign
vendored
2
Dependencies/AltSign
vendored
Submodule Dependencies/AltSign updated: 5905038272...db6cadf021
Reference in New Issue
Block a user