Adds "Enable JIT" context menu action for active apps

Allows users to manually enable JIT for apps that don't explicitly support AltKit.
This commit is contained in:
Riley Testut
2021-09-03 13:57:15 -05:00
parent e4b0b153e5
commit 9c72b7ae8f
5 changed files with 239 additions and 0 deletions

View File

@@ -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)
}
}
}

View File

@@ -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, Error>) -> 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]

View File

@@ -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

View File

@@ -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<Context: EnableJITContext>: ResultOperation<Void>
{
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<Void, Error> as the value type instead.
let result = Future<Result<Void, Error>, 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)
}
}
}
}
}
}
}
}