Adds support for activating and deactivating apps

iOS 13.3.1 limits free developer accounts to 3 apps and app extensions. As a workaround, we now allow up to 3 “active” apps (apps with installed provisioning profiles), as well as additional “inactivate” apps which don’t have any profiles installed, causing them to not count towards the total. Inactive apps cannot be opened until they are activated.
This commit is contained in:
Riley Testut
2020-03-11 14:43:19 -07:00
parent 06fed802b1
commit bc02cfc8a9
16 changed files with 771 additions and 152 deletions

View File

@@ -228,6 +228,17 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
team.isActiveTeam = false
}
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
{
// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1.
UserDefaults.standard.activeAppsLimit = 3
}
else
{
UserDefaults.standard.activeAppsLimit = nil
}
// Save
try context.save()

View File

@@ -0,0 +1,90 @@
//
// DeactivateAppOperation.swift
// AltStore
//
// Created by Riley Testut on 3/4/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltSign
import AltKit
import Roxas
@objc(DeactivateAppOperation)
class DeactivateAppOperation: ResultOperation<InstalledApp>
{
let app: InstalledApp
let context: OperationContext
init(app: InstalledApp, context: OperationContext)
{
self.app = app
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 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)) }
ServerManager.shared.connect(to: server) { (result) in
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success(let connection):
print("Sending deactivate app request...")
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
let allIdentifiers = [installedApp.resignedBundleIdentifier] + appExtensionProfiles
let request = RemoveProvisioningProfilesRequest(udid: udid, bundleIdentifiers: Set(allIdentifiers))
connection.send(request) { (result) in
print("Sent deactive app request!")
switch result
{
case .failure(let error): self.finish(.failure(error))
case .success:
print("Waiting for deactivate app response...")
connection.receiveResponse() { (result) in
print("Receiving deactivate 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(.removeProvisioningProfiles):
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
self.progress.completedUnitCount += 1
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
installedApp.isActive = false
self.finish(.success(installedApp))
}
case .success: self.finish(.failure(ALTServerError(.unknownResponse)))
}
}
}
}
}
}
}
}
}

View File

@@ -111,7 +111,40 @@ class InstallAppOperation: ResultOperation<InstalledApp>
self.context.beginInstallationHandler?(installedApp)
let request = BeginInstallationRequest()
var activeProfiles: Set<String>?
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
{
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
let fetchRequest = InstalledApp.activeAppsFetchRequest()
fetchRequest.includesPendingChanges = false
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"
if requiredActiveAppSlots <= availableActiveApps
{
// This app has not been explicitly activated, but there are enough slots available,
// so implicitly activate it.
installedApp.isActive = true
activeApps.append(installedApp)
}
else
{
installedApp.isActive = false
}
}
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
})
}
let request = BeginInstallationRequest(activeProfiles: activeProfiles)
connection.send(request) { (result) in
switch result
{

View File

@@ -58,10 +58,10 @@ class RefreshAppOperation: ResultOperation<InstalledApp>
print("Sending refresh app request...")
var activeProfiles: Set<String>?
if team.type == .free
if UserDefaults.standard.activeAppsLimit != nil
{
let activeApps = InstalledApp.all(in: context)
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
let activeApps = InstalledApp.fetchActiveApps(in: context)
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles