mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Asks user to deactivate an app when installing app without available active slot
When attempting to install a new app without any active slots available, AltStore will now present an alert asking the user to choose an app to deactivate in order to continue installation — just like when activating an app without an active slot.
This commit is contained in:
@@ -230,6 +230,73 @@ extension AppManager
|
|||||||
|
|
||||||
return authenticationOperation
|
return authenticationOperation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deactivateApps(for app: ALTApplication, presentingViewController: UIViewController, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
{
|
||||||
|
guard let activeAppsLimit = UserDefaults.standard.activeAppsLimit else { return completion(.success(())) }
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext)
|
||||||
|
.filter { $0.bundleIdentifier != app.bundleIdentifier } // Don't count app towards total if it matches activating app
|
||||||
|
.sorted { ($0.name, $0.refreshedDate) < ($1.name, $1.refreshedDate) }
|
||||||
|
|
||||||
|
var title: String = NSLocalizedString("Cannot Activate More than 3 Apps", comment: "")
|
||||||
|
let message: String
|
||||||
|
|
||||||
|
if UserDefaults.standard.activeAppLimitIncludesExtensions
|
||||||
|
{
|
||||||
|
if app.appExtensions.isEmpty
|
||||||
|
{
|
||||||
|
message = NSLocalizedString("Non-developer Apple IDs 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 = app.appExtensions.count == 1 ? NSLocalizedString("app extension", comment: "") : NSLocalizedString("app extensions", comment: "")
|
||||||
|
message = String(format: NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions, and “%@” contains %@ %@. Please choose an app to deactivate.", comment: ""), app.name, NSNumber(value: app.appExtensions.count), appExtensionText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message = NSLocalizedString("Non-developer Apple IDs 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)
|
||||||
|
let requiredActiveSlots = UserDefaults.standard.activeAppLimitIncludesExtensions ? (1 + app.appExtensions.count) : 1
|
||||||
|
guard requiredActiveSlots > availableActiveApps else { return completion(.success(())) }
|
||||||
|
|
||||||
|
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||||
|
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { (action) in
|
||||||
|
completion(.failure(OperationError.cancelled))
|
||||||
|
})
|
||||||
|
|
||||||
|
for activeApp in activeApps where activeApp.bundleIdentifier != StoreApp.altstoreAppID
|
||||||
|
{
|
||||||
|
alertController.addAction(UIAlertAction(title: activeApp.name, style: .default) { (action) in
|
||||||
|
activeApp.isActive = false
|
||||||
|
|
||||||
|
self.deactivate(activeApp, presentingViewController: presentingViewController) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error):
|
||||||
|
activeApp.managedObjectContext?.perform {
|
||||||
|
activeApp.isActive = true
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
case .success:
|
||||||
|
self.deactivateApps(for: app, presentingViewController: presentingViewController, completion: completion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
presentingViewController.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppManager
|
extension AppManager
|
||||||
@@ -770,7 +837,7 @@ private extension AppManager
|
|||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
private func _install(_ app: AppProtocol, operation appOperation: 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 progress = Progress.discreteProgress(totalUnitCount: 100)
|
||||||
|
|
||||||
@@ -778,7 +845,7 @@ private extension AppManager
|
|||||||
assert(context.authenticatedContext === group.context)
|
assert(context.authenticatedContext === group.context)
|
||||||
|
|
||||||
context.beginInstallationHandler = { (installedApp) in
|
context.beginInstallationHandler = { (installedApp) in
|
||||||
switch operation
|
switch appOperation
|
||||||
{
|
{
|
||||||
case .update where installedApp.bundleIdentifier == StoreApp.altstoreAppID:
|
case .update where installedApp.bundleIdentifier == StoreApp.altstoreAppID:
|
||||||
// AltStore will quit before installation finishes,
|
// AltStore will quit before installation finishes,
|
||||||
@@ -843,6 +910,43 @@ private extension AppManager
|
|||||||
verifyOperation.addDependency(downloadOperation)
|
verifyOperation.addDependency(downloadOperation)
|
||||||
|
|
||||||
|
|
||||||
|
/* Deactivate Apps (if necessary) */
|
||||||
|
let deactivateAppsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Only attempt to deactivate apps if we're installing a new app.
|
||||||
|
// We handle deactivating apps separately when activating an app.
|
||||||
|
guard case .install = appOperation else {
|
||||||
|
operation.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = context.error
|
||||||
|
{
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let app = context.app, let presentingViewController = context.authenticatedContext.presentingViewController else { throw OperationError.invalidParameters }
|
||||||
|
|
||||||
|
self?.deactivateApps(for: app, presentingViewController: presentingViewController) { result in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): group.context.error = error
|
||||||
|
case .success: break
|
||||||
|
}
|
||||||
|
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
group.context.error = error
|
||||||
|
operation.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deactivateAppsOperation.addDependency(verifyOperation)
|
||||||
|
|
||||||
|
|
||||||
/* Refresh Anisette Data */
|
/* Refresh Anisette Data */
|
||||||
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
|
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
|
||||||
refreshAnisetteDataOperation.resultHandler = { (result) in
|
refreshAnisetteDataOperation.resultHandler = { (result) in
|
||||||
@@ -852,7 +956,7 @@ private extension AppManager
|
|||||||
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
|
case .success(let anisetteData): group.context.session?.anisetteData = anisetteData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshAnisetteDataOperation.addDependency(verifyOperation)
|
refreshAnisetteDataOperation.addDependency(deactivateAppsOperation)
|
||||||
|
|
||||||
|
|
||||||
/* Fetch Provisioning Profiles */
|
/* Fetch Provisioning Profiles */
|
||||||
@@ -921,7 +1025,7 @@ private extension AppManager
|
|||||||
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
|
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
|
||||||
installOperation.addDependency(sendAppOperation)
|
installOperation.addDependency(sendAppOperation)
|
||||||
|
|
||||||
let operations = [downloadOperation, verifyOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation]
|
let operations = [downloadOperation, verifyOperation, deactivateAppsOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation]
|
||||||
group.add(operations)
|
group.add(operations)
|
||||||
self.run(operations, context: group.context)
|
self.run(operations, context: group.context)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
import Intents
|
import Intents
|
||||||
|
import Combine
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
@@ -1038,46 +1039,76 @@ private extension MyAppsViewController
|
|||||||
|
|
||||||
func activate(_ installedApp: InstalledApp)
|
func activate(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
func activate()
|
func finish(_ result: Result<InstalledApp, Error>)
|
||||||
{
|
{
|
||||||
installedApp.isActive = true
|
do
|
||||||
|
{
|
||||||
AppManager.shared.activate(installedApp, presentingViewController: self) { (result) in
|
let app = try result.get()
|
||||||
do
|
app.managedObjectContext?.perform {
|
||||||
{
|
|
||||||
let app = try result.get()
|
|
||||||
try? app.managedObjectContext?.save()
|
try? app.managedObjectContext?.save()
|
||||||
}
|
}
|
||||||
catch
|
}
|
||||||
{
|
catch OperationError.cancelled
|
||||||
print("Failed to activate app:", error)
|
{
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to activate app:", error)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
installedApp.isActive = false
|
installedApp.isActive = false
|
||||||
|
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if UserDefaults.standard.activeAppsLimit != nil
|
if UserDefaults.standard.activeAppsLimit != nil, #available(iOS 13, *)
|
||||||
{
|
{
|
||||||
self.deactivateApps(for: installedApp) { (shouldContinue) in
|
// UserDefaults.standard.activeAppsLimit is only non-nil on iOS 13.3.1 or later, so the #available check is just so we can use Combine.
|
||||||
if shouldContinue
|
|
||||||
{
|
guard let app = ALTApplication(fileURL: installedApp.fileURL) else { return finish(.failure(OperationError.invalidApp)) }
|
||||||
activate()
|
|
||||||
|
var cancellable: AnyCancellable?
|
||||||
|
cancellable = DatabaseManager.shared.viewContext.registeredObjects.publisher
|
||||||
|
.compactMap { $0 as? InstalledApp }
|
||||||
|
.filter(\.isActive)
|
||||||
|
.map { $0.publisher(for: \.isActive) }
|
||||||
|
.collect()
|
||||||
|
.flatMap { publishers in
|
||||||
|
Publishers.MergeMany(publishers)
|
||||||
}
|
}
|
||||||
else
|
.first { isActive in !isActive }
|
||||||
{
|
.sink { _ in
|
||||||
installedApp.isActive = false
|
// A previously active app is now inactive,
|
||||||
|
// which means there are now enough slots to activate the app,
|
||||||
|
// so pre-emptively mark it as active to provide visual feedback sooner.
|
||||||
|
installedApp.isActive = true
|
||||||
|
cancellable?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
AppManager.shared.deactivateApps(for: app, presentingViewController: self) { result in
|
||||||
|
cancellable?.cancel()
|
||||||
|
installedApp.managedObjectContext?.perform {
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error):
|
||||||
|
installedApp.isActive = false
|
||||||
|
finish(.failure(error))
|
||||||
|
|
||||||
|
case .success:
|
||||||
|
installedApp.isActive = true
|
||||||
|
AppManager.shared.activate(installedApp, presentingViewController: self, completionHandler: finish(_:))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
activate()
|
installedApp.isActive = true
|
||||||
|
AppManager.shared.activate(installedApp, presentingViewController: self, completionHandler: finish(_:))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1110,75 +1141,6 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deactivateApps(for installedApp: InstalledApp, completion: @escaping (Bool) -> Void)
|
|
||||||
{
|
|
||||||
guard let activeAppsLimit = UserDefaults.standard.activeAppsLimit else { return completion(true) }
|
|
||||||
|
|
||||||
let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext)
|
|
||||||
.filter { $0.bundleIdentifier != installedApp.bundleIdentifier } // Don't count app towards total if it matches activating app
|
|
||||||
|
|
||||||
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.requiredActiveSlots > availableActiveApps else { return completion(true) }
|
|
||||||
|
|
||||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
|
||||||
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { (action) in
|
|
||||||
completion(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
for app in activeApps where app.bundleIdentifier != StoreApp.altstoreAppID
|
|
||||||
{
|
|
||||||
alertController.addAction(UIAlertAction(title: app.name, style: .default) { (action) in
|
|
||||||
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.
|
|
||||||
installedApp.isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
self.deactivate(app) { (result) in
|
|
||||||
installedApp.managedObjectContext?.perform {
|
|
||||||
switch result
|
|
||||||
{
|
|
||||||
case .failure:
|
|
||||||
installedApp.isActive = false
|
|
||||||
completion(false)
|
|
||||||
|
|
||||||
case .success:
|
|
||||||
self.deactivateApps(for: installedApp, completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
self.present(alertController, animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func remove(_ installedApp: InstalledApp)
|
func remove(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
let title = String(format: NSLocalizedString("Remove “%@” from AltStore?", comment: ""), installedApp.name)
|
let title = String(format: NSLocalizedString("Remove “%@” from AltStore?", comment: ""), installedApp.name)
|
||||||
|
|||||||
Reference in New Issue
Block a user