From 9c72b7ae8f7217fe4224d30a0083f3a172f31f40 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 3 Sep 2021 13:57:15 -0500 Subject: [PATCH] Adds "Enable JIT" context menu action for active apps Allows users to manually enable JIT for apps that don't explicitly support AltKit. --- AltStore.xcodeproj/project.pbxproj | 8 ++ AltStore/Extensions/UIDevice+Vibration.swift | 46 +++++++ AltStore/Managing Apps/AppManager.swift | 23 ++++ AltStore/My Apps/MyAppsViewController.swift | 26 ++++ AltStore/Operations/EnableJITOperation.swift | 136 +++++++++++++++++++ 5 files changed, 239 insertions(+) create mode 100644 AltStore/Extensions/UIDevice+Vibration.swift create mode 100644 AltStore/Operations/EnableJITOperation.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 0ec0f542..020eba11 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -347,6 +347,8 @@ BFF7C90F257844C900E55F36 /* AltXPC.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = BFF7C904257844C900E55F36 /* AltXPC.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; BFF7C920257844FA00E55F36 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; }; BFF7C9342578492100E55F36 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */; }; + D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */; }; + D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */; }; D58D5F2E26DFE68E00E55E38 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = D58D5F2D26DFE68E00E55E38 /* LaunchAtLogin */; }; /* End PBXBuildFile section */ @@ -795,6 +797,8 @@ BFF7EC4C25081E9300BDE521 /* AltStore 8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 8.xcdatamodel"; sourceTree = ""; }; BFFCFA45248835530077BFCE /* AltDaemon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltDaemon.entitlements; sourceTree = ""; }; C9EEAA842DA87A88A870053B /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableJITOperation.swift; sourceTree = ""; }; + D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Vibration.swift"; sourceTree = ""; }; EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.debug.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.debug.xcconfig"; sourceTree = ""; }; FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltServer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -1556,6 +1560,7 @@ BF663C4E2433ED8200DAA738 /* FileManager+DirectorySize.swift */, BF8CAE4D248AEABA004D6CCE /* UIDevice+Jailbreak.swift */, BFE00A1F2503097F00EB4D0C /* INInteraction+AltStore.swift */, + D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */, ); path = Extensions; sourceTree = ""; @@ -1620,6 +1625,7 @@ BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */, BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */, BFF00D312501BDA100746320 /* BackgroundRefreshAppsOperation.swift */, + D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */, ); path = Operations; sourceTree = ""; @@ -2482,10 +2488,12 @@ BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */, BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */, BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */, + D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */, BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */, BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */, BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */, + D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */, BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */, BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */, BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */, diff --git a/AltStore/Extensions/UIDevice+Vibration.swift b/AltStore/Extensions/UIDevice+Vibration.swift new file mode 100644 index 00000000..3295e3c2 --- /dev/null +++ b/AltStore/Extensions/UIDevice+Vibration.swift @@ -0,0 +1,46 @@ +// +// UIDevice+Vibration.swift +// AltStore +// +// Created by Riley Testut on 9/1/21. +// Copyright © 2021 Riley Testut. All rights reserved. +// + +import AudioToolbox +import CoreHaptics + +private extension SystemSoundID +{ + static let pop = SystemSoundID(1520) + static let cancelled = SystemSoundID(1521) + static let tryAgain = SystemSoundID(1102) +} + +@available(iOS 13, *) +extension UIDevice +{ + enum VibrationPattern + { + case success + case error + } +} + +@available(iOS 13, *) +extension UIDevice +{ + var isVibrationSupported: Bool { + return CHHapticEngine.capabilitiesForHardware().supportsHaptics + } + + func vibrate(pattern: VibrationPattern) + { + guard self.isVibrationSupported else { return } + + switch pattern + { + case .success: AudioServicesPlaySystemSound(.tryAgain) + case .error: AudioServicesPlaySystemSound(.cancelled) + } + } +} diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index d27e5ceb..daafda82 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -23,6 +23,7 @@ extension AppManager static let didFetchSourceNotification = Notification.Name("com.altstore.AppManager.didFetchSource") static let expirationWarningNotificationID = "altstore-expiration-warning" + static let enableJITResultNotificationID = "altstore-enable-jit" } @available(iOS 13, *) @@ -540,6 +541,28 @@ extension AppManager self.run([removeAppOperation, removeAppBackupOperation], context: authenticationContext) } + @available(iOS 14, *) + func enableJIT(for installedApp: InstalledApp, completionHandler: @escaping (Result) -> Void) + { + class Context: OperationContext, EnableJITContext + { + var installedApp: InstalledApp? + } + + let context = Context() + context.installedApp = installedApp + + let findServerOperation = self.findServer(context: context) { _ in } + + let enableJITOperation = EnableJITOperation(context: context) + enableJITOperation.resultHandler = { (result) in + completionHandler(result) + } + enableJITOperation.addDependency(findServerOperation) + + self.run([enableJITOperation], context: context, requiresSerialQueue: true) + } + func installationProgress(for app: AppProtocol) -> Progress? { let progress = self.installationProgress[app.bundleIdentifier] diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 0d1e4585..987e8f8c 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -1349,6 +1349,22 @@ private extension MyAppsViewController } } } + + @available(iOS 14, *) + func enableJIT(for installedApp: InstalledApp) + { + AppManager.shared.enableJIT(for: installedApp) { result in + DispatchQueue.main.async { + switch result + { + case .success: break + case .failure(let error): + let toastView = ToastView(error: error) + toastView.show(in: self) + } + } + } + } } private extension MyAppsViewController @@ -1555,6 +1571,11 @@ extension MyAppsViewController self.remove(installedApp) } + let jitAction = UIAction(title: NSLocalizedString("Enable JIT", comment: ""), image: UIImage(systemName: "bolt")) { (action) in + guard #available(iOS 14, *) else { return } + self.enableJIT(for: installedApp) + } + let backupAction = UIAction(title: NSLocalizedString("Back Up", comment: ""), image: UIImage(systemName: "doc.on.doc")) { (action) in self.backup(installedApp) } @@ -1601,6 +1622,11 @@ extension MyAppsViewController actions.append(activateAction) } + if installedApp.isActive, #available(iOS 14, *) + { + actions.append(jitAction) + } + #if BETA actions.append(changeIconMenu) #endif diff --git a/AltStore/Operations/EnableJITOperation.swift b/AltStore/Operations/EnableJITOperation.swift new file mode 100644 index 00000000..9aa5ebee --- /dev/null +++ b/AltStore/Operations/EnableJITOperation.swift @@ -0,0 +1,136 @@ +// +// EnableJITOperation.swift +// EnableJITOperation +// +// Created by Riley Testut on 9/1/21. +// Copyright © 2021 Riley Testut. All rights reserved. +// + +import UIKit +import Combine + +import AltStoreCore + +@available(iOS 14, *) +protocol EnableJITContext +{ + var server: Server? { get } + var installedApp: InstalledApp? { get } + + var error: Error? { get } +} + +@available(iOS 14, *) +class EnableJITOperation: ResultOperation +{ + let context: Context + + private var cancellable: AnyCancellable? + + init(context: Context) + { + self.context = context + } + + 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 { + guard let bundle = Bundle(url: installedApp.fileURL), + let processName = bundle.executableURL?.lastPathComponent + else { return self.finish(.failure(OperationError.invalidApp)) } + + let appName = installedApp.name + let openAppURL = installedApp.openAppURL + + ServerManager.shared.connect(to: server) { result in + switch result + { + case .failure(let error): self.finish(.failure(error)) + case .success(let connection): + print("Sending enable JIT request...") + + DispatchQueue.main.async { + + // Launch app to make sure it is running in foreground. + UIApplication.shared.open(openAppURL) { success in + guard success else { return self.finish(.failure(OperationError.openAppFailed(name: appName))) } + + // Combine immediately finishes if an error is thrown, but we want to wait at least until app enters background. + // As a workaround, we set error type to Never and use Result as the value type instead. + let result = Future, Never> { promise in + let request = EnableUnsignedCodeExecutionRequest(udid: udid, processName: processName) + connection.send(request) { result in + print("Sent enable JIT request!") + + switch result + { + case .failure(let error): promise(.success(.failure(error))) + case .success: + print("Waiting for enable JIT response...") + connection.receiveResponse() { result in + print("Received enable JIT response:", result) + + switch result + { + case .failure(let error): promise(.success(.failure(error))) + case .success(.error(let response)): promise(.success(.failure(response.error))) + case .success(.enableUnsignedCodeExecution): promise(.success(.success(()))) + case .success: promise(.success(.failure(ALTServerError(.unknownResponse)))) + } + } + } + } + } + + //TODO: Handle case where app does not enter background (e.g. iPad multitasking). + self.cancellable = result + .combineLatest(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification, object: nil)) + .first() + .receive(on: DispatchQueue.main) + .sink { (result, _) in + let content = UNMutableNotificationContent() + + switch result + { + case .failure(let error): + content.title = String(format: NSLocalizedString("Could not enable JIT for %@", comment: ""), appName) + content.body = error.localizedDescription + + UIDevice.current.vibrate(pattern: .error) + + case .success: + content.title = String(format: NSLocalizedString("Enabled JIT for %@", comment: ""), appName) + content.body = String(format: NSLocalizedString("JIT will remain enabled until you quit the app.", comment: "")) + + UIDevice.current.vibrate(pattern: .success) + } + + if UIApplication.shared.applicationState == .background + { + // For some reason, notification won't show up reliably unless we provide a trigger (as of iOS 15). + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) + + let request = UNNotificationRequest(identifier: AppManager.enableJITResultNotificationID, content: content, trigger: trigger) + UNUserNotificationCenter.current().add(request) + } + + self.finish(result) + } + } + } + } + } + } + } +}