diff --git a/AltServer/AppDelegate.swift b/AltServer/AppDelegate.swift index 03e0c9ed..aeab490b 100644 --- a/AltServer/AppDelegate.swift +++ b/AltServer/AppDelegate.swift @@ -7,20 +7,203 @@ // import Cocoa +import UserNotifications + +import AltSign @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { + private var statusItem: NSStatusItem? + + private var connectedDevices = [ALTDevice]() + + private weak var authenticationAlert: NSAlert? + + @IBOutlet private var appMenu: NSMenu! + @IBOutlet private var connectedDevicesMenu: NSMenu! + + private weak var authenticationAppleIDTextField: NSTextField? + private weak var authenticationPasswordTextField: NSSecureTextField? - - func applicationDidFinishLaunching(_ aNotification: Notification) { - // Insert code here to initialize your application + func applicationDidFinishLaunching(_ aNotification: Notification) + { + UNUserNotificationCenter.current().delegate = self + ConnectionManager.shared.start() + + let item = NSStatusBar.system.statusItem(withLength: -1) + guard let button = item.button else { return } + + button.image = NSImage(named: "MenuBarIcon") + button.target = self + button.action = #selector(AppDelegate.presentMenu) + + self.statusItem = item + + self.connectedDevicesMenu.delegate = self } - func applicationWillTerminate(_ aNotification: Notification) { + func applicationWillTerminate(_ aNotification: Notification) + { // Insert code here to tear down your application } - - } +private extension AppDelegate +{ + @objc func presentMenu() + { + guard let button = self.statusItem?.button, let superview = button.superview, let window = button.window else { return } + + self.connectedDevices = ALTDeviceManager.shared.connectedDevices + + let x = button.frame.origin.x + let y = button.frame.origin.y - 5 + + let location = superview.convert(NSMakePoint(x, y), to: nil) + + guard let event = NSEvent.mouseEvent(with: .leftMouseUp, location: location, + modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil, + eventNumber: 0, clickCount: 1, pressure: 0) + else { return } + + NSMenu.popUpContextMenu(self.appMenu, with: event, for: button) + } + + @objc func installAltStore(_ item: NSMenuItem) + { + guard case let index = self.connectedDevicesMenu.index(of: item), index != -1 else { return } + + let alert = NSAlert() + alert.messageText = NSLocalizedString("Please enter your Apple ID and password.", comment: "") + alert.informativeText = NSLocalizedString("Your Apple ID and password are not saved and are only sent to Apple for authentication.", comment: "") + + let textFieldSize = NSSize(width: 300, height: 22) + + let appleIDTextField = NSTextField(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height)) + appleIDTextField.delegate = self + appleIDTextField.translatesAutoresizingMaskIntoConstraints = false + appleIDTextField.placeholderString = NSLocalizedString("Apple ID", comment: "") + alert.window.initialFirstResponder = appleIDTextField + self.authenticationAppleIDTextField = appleIDTextField + + let passwordTextField = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height)) + passwordTextField.delegate = self + passwordTextField.translatesAutoresizingMaskIntoConstraints = false + passwordTextField.placeholderString = NSLocalizedString("Password", comment: "") + self.authenticationPasswordTextField = passwordTextField + + appleIDTextField.nextKeyView = passwordTextField + + let stackView = NSStackView(frame: NSRect(x: 0, y: 0, width: textFieldSize.width, height: textFieldSize.height * 2)) + stackView.orientation = .vertical + stackView.distribution = .equalSpacing + stackView.spacing = 0 + stackView.addArrangedSubview(appleIDTextField) + stackView.addArrangedSubview(passwordTextField) + alert.accessoryView = stackView + + alert.addButton(withTitle: NSLocalizedString("Install", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) + + self.authenticationAlert = alert + self.validate() + + NSRunningApplication.current.activate(options: .activateIgnoringOtherApps) + + let response = alert.runModal() + guard response == .alertFirstButtonReturn else { return } + + let username = appleIDTextField.stringValue + let password = passwordTextField.stringValue + + let device = self.connectedDevices[index] + ALTDeviceManager.shared.installAltStore(to: device, appleID: username, password: password) { (result) in + let content = UNMutableNotificationContent() + + switch result + { + case .success: + content.title = NSLocalizedString("Installation Succeeded", comment: "") + content.body = String(format: NSLocalizedString("AltStore was successfully installed on %@.", comment: ""), device.name) + + case .failure(let error): + content.title = NSLocalizedString("Installation Failed", comment: "") + content.body = error.localizedDescription + } + + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) + } + } +} + +extension AppDelegate: NSMenuDelegate +{ + func numberOfItems(in menu: NSMenu) -> Int + { + return self.connectedDevices.isEmpty ? 1 : self.connectedDevices.count + } + + func menu(_ menu: NSMenu, update item: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool + { + if self.connectedDevices.isEmpty + { + item.title = NSLocalizedString("No Connected Devices", comment: "") + item.isEnabled = false + item.target = nil + item.action = nil + } + else + { + let device = self.connectedDevices[index] + item.title = device.name + item.isEnabled = true + item.target = self + item.action = #selector(AppDelegate.installAltStore) + item.tag = index + } + + return true + } +} + +extension AppDelegate: NSTextFieldDelegate +{ + func controlTextDidChange(_ obj: Notification) + { + self.validate() + } + + func controlTextDidEndEditing(_ obj: Notification) + { + self.validate() + } + + private func validate() + { + guard + let appleID = self.authenticationAppleIDTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines), + let password = self.authenticationPasswordTextField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) + else { return } + + if appleID.isEmpty || password.isEmpty + { + self.authenticationAlert?.buttons.first?.isEnabled = false + } + else + { + self.authenticationAlert?.buttons.first?.isEnabled = true + } + + self.authenticationAlert?.layout() + } +} + +extension AppDelegate: UNUserNotificationCenterDelegate +{ + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) + { + completionHandler([.alert, .sound, .badge]) + } +} diff --git a/AltServer/Assets.xcassets/MenuBarIcon.imageset/Contents.json b/AltServer/Assets.xcassets/MenuBarIcon.imageset/Contents.json new file mode 100644 index 00000000..69ff52d9 --- /dev/null +++ b/AltServer/Assets.xcassets/MenuBarIcon.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "MenuBarIcon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "MenuBarIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/AltServer/Assets.xcassets/MenuBarIcon.imageset/MenuBarIcon.png b/AltServer/Assets.xcassets/MenuBarIcon.imageset/MenuBarIcon.png new file mode 100644 index 00000000..e13b1f0d Binary files /dev/null and b/AltServer/Assets.xcassets/MenuBarIcon.imageset/MenuBarIcon.png differ diff --git a/AltServer/Assets.xcassets/MenuBarIcon.imageset/MenuBarIcon@2x.png b/AltServer/Assets.xcassets/MenuBarIcon.imageset/MenuBarIcon@2x.png new file mode 100644 index 00000000..ca13e7fc Binary files /dev/null and b/AltServer/Assets.xcassets/MenuBarIcon.imageset/MenuBarIcon@2x.png differ diff --git a/AltServer/Base.lproj/Main.storyboard b/AltServer/Base.lproj/Main.storyboard index e0524817..bd382187 100644 --- a/AltServer/Base.lproj/Main.storyboard +++ b/AltServer/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ - + @@ -8,6 +8,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -21,98 +76,27 @@ - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -337,325 +321,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - Default - - - - - - - Left to Right - - - - - - - Right to Left - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -674,170 +339,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NSAllRomanInputSourcesLocaleIdentifier - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/AltServer/Connections/ConnectionManager.swift b/AltServer/Connections/ConnectionManager.swift index 31675c5f..d3e74ad1 100644 --- a/AltServer/Connections/ConnectionManager.swift +++ b/AltServer/Connections/ConnectionManager.swift @@ -277,7 +277,7 @@ private extension ConnectionManager print("Processed app data!") - guard ALTDeviceManager.shared.connectedDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) } + guard ALTDeviceManager.shared.availableDevices.contains(where: { $0.identifier == request.udid }) else { throw ALTServerError(.deviceNotFound) } print("Writing app data...") diff --git a/AltServer/ViewController.swift b/AltServer/Devices/ALTDeviceManager+Installation.swift similarity index 83% rename from AltServer/ViewController.swift rename to AltServer/Devices/ALTDeviceManager+Installation.swift index 815e06a7..320567e8 100644 --- a/AltServer/ViewController.swift +++ b/AltServer/Devices/ALTDeviceManager+Installation.swift @@ -1,12 +1,13 @@ // -// ViewController.swift +// ALTDeviceManager+Installation.swift // AltServer // -// Created by Riley Testut on 5/24/19. +// Created by Riley Testut on 7/1/19. // Copyright © 2019 Riley Testut. All rights reserved. // import Cocoa +import UserNotifications enum InstallError: Error { @@ -26,119 +27,39 @@ enum InstallError: Error } } -class ViewController: NSViewController +extension ALTDeviceManager { - @IBOutlet private var emailAddressTextField: NSTextField! - @IBOutlet private var passwordTextField: NSSecureTextField! - - @IBOutlet private var devicesButton: NSPopUpButton! - - private var currentDevice: ALTDevice? - - override func viewDidLoad() - { - super.viewDidLoad() - - ConnectionManager.shared.stateUpdateHandler = { (state) in - DispatchQueue.main.async { - switch state - { - case .notRunning: self.view.window?.title = "" - case .connecting: self.view.window?.title = "Connecting...." - case .running(let service): self.view.window?.title = service.name ?? "" - case .failed(let error): self.view.window?.title = error.localizedDescription - } - } - } - - ConnectionManager.shared.start() - - self.update() - } - - func update() - { - self.devicesButton.removeAllItems() - - let devices = ALTDeviceManager.shared.connectedDevices - - if devices.isEmpty - { - self.devicesButton.addItem(withTitle: "No Connected Device") - } - else - { - for device in devices - { - self.devicesButton.addItem(withTitle: device.name) - } - } - - if let currentDevice = self.currentDevice, let index = devices.firstIndex(of: currentDevice) - { - self.devicesButton.selectItem(at: index) - } - else - { - self.currentDevice = devices.first - self.devicesButton.selectItem(at: 0) - } - } -} - -private extension ViewController -{ - @IBAction func installAltStore(_ sender: NSButton) - { - guard let device = self.currentDevice else { return } - guard !self.emailAddressTextField.stringValue.isEmpty, !self.passwordTextField.stringValue.isEmpty else { return } - - self.installAltStore(to: device) - } - - @IBAction func chooseDevice(_ sender: NSPopUpButton) - { - let devices = ALTDeviceManager.shared.connectedDevices - guard !devices.isEmpty else { return } - - let index = sender.indexOfSelectedItem - - let device = devices[index] - self.currentDevice = device - } -} - -private extension ViewController -{ - func installAltStore(to device: ALTDevice) + func installAltStore(to device: ALTDevice, appleID: String, password: String, completion: @escaping (Result) -> Void) { let destinationDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) func finish(_ error: Error?, title: String = "") { DispatchQueue.main.async { - let alert = NSAlert() - if let error = error { - alert.messageText = title - alert.informativeText = error.localizedDescription + completion(.failure(error)) } else { - alert.messageText = NSLocalizedString("Successfully installed AltStore!", comment: "") + completion(.success(())) } - - alert.runModal() } try? FileManager.default.removeItem(at: destinationDirectoryURL) } - self.authenticate() { (result) in + self.authenticate(appleID: appleID, password: password) { (result) in do { let account = try result.get() + + let content = UNMutableNotificationContent() + content.title = String(format: NSLocalizedString("Installing AltStore to %@...", comment: ""), device.name) + + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) + self.fetchTeam(for: account) { (result) in do { @@ -253,9 +174,9 @@ private extension ViewController downloadTask.resume() } - func authenticate(completionHandler: @escaping (Result) -> Void) + func authenticate(appleID: String, password: String, completionHandler: @escaping (Result) -> Void) { - ALTAppleAPI.shared.authenticate(appleID: self.emailAddressTextField.stringValue, password: self.passwordTextField.stringValue) { (account, error) in + ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in let result = Result(account, error) completionHandler(result) } diff --git a/AltServer/Devices/ALTDeviceManager.h b/AltServer/Devices/ALTDeviceManager.h index d3997432..8447b57d 100644 --- a/AltServer/Devices/ALTDeviceManager.h +++ b/AltServer/Devices/ALTDeviceManager.h @@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN @property (class, nonatomic, readonly) ALTDeviceManager *sharedManager; @property (nonatomic, readonly) NSArray *connectedDevices; +@property (nonatomic, readonly) NSArray *availableDevices; - (NSProgress *)installAppAtURL:(NSURL *)fileURL toDeviceWithUDID:(NSString *)udid completionHandler:(void (^)(BOOL success, NSError *_Nullable error))completionHandler; diff --git a/AltServer/Devices/ALTDeviceManager.mm b/AltServer/Devices/ALTDeviceManager.mm index 7d5bf67c..2d0dffa5 100644 --- a/AltServer/Devices/ALTDeviceManager.mm +++ b/AltServer/Devices/ALTDeviceManager.mm @@ -518,7 +518,17 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError"; - (NSArray *)connectedDevices { - NSMutableArray *connectedDevices = [NSMutableArray array]; + return [self availableDevicesIncludingNetworkDevices:NO]; +} + +- (NSArray *)availableDevices +{ + return [self availableDevicesIncludingNetworkDevices:YES]; +} + +- (NSArray *)availableDevicesIncludingNetworkDevices:(BOOL)includingNetworkDevices +{ + NSMutableSet *connectedDevices = [NSMutableSet set]; int count = 0; char **udids = NULL; @@ -533,11 +543,18 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError"; char *udid = udids[i]; idevice_t device = NULL; - idevice_new(&device, udid); + + if (includingNetworkDevices) + { + idevice_new(&device, udid); + } + else + { + idevice_new_ignore_network(&device, udid); + } if (!device) { - fprintf(stderr, "ERROR: No device with UDID %s attached.\n", udid); continue; } @@ -580,7 +597,7 @@ NSErrorDomain const ALTDeviceErrorDomain = @"com.rileytestut.ALTDeviceError"; idevice_device_list_free(udids); - return connectedDevices; + return connectedDevices.allObjects; } @end diff --git a/AltServer/Info.plist b/AltServer/Info.plist index 80297304..dc24eecc 100644 --- a/AltServer/Info.plist +++ b/AltServer/Info.plist @@ -26,6 +26,8 @@ Copyright © 2019 Riley Testut. All rights reserved. NSMainStoryboardFile Main + LSUIElement + NSPrincipalClass NSApplication diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index c53517f9..cf2b24e6 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -19,10 +19,10 @@ 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 */; }; + BF3F786422CAA41E008FBD20 /* ALTDeviceManager+Installation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3F786322CAA41E008FBD20 /* ALTDeviceManager+Installation.swift */; }; BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF43002D22A714AF0051E2BC /* Keychain.swift */; }; BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */; }; BF458690229872EA00BD7491 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF45868F229872EA00BD7491 /* AppDelegate.swift */; }; - BF458692229872EA00BD7491 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF458691229872EA00BD7491 /* ViewController.swift */; }; BF458694229872EA00BD7491 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF458693229872EA00BD7491 /* Assets.xcassets */; }; BF458697229872EA00BD7491 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF458695229872EA00BD7491 /* Main.storyboard */; }; BF4586C52298CDB800BD7491 /* ALTDeviceManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = BF4586C42298CDB800BD7491 /* ALTDeviceManager.mm */; }; @@ -241,11 +241,11 @@ BF1E314822A060F400370A3C /* NSError+ALTServerError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSError+ALTServerError.h"; sourceTree = ""; }; BF1E314922A060F400370A3C /* NSError+ALTServerError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSError+ALTServerError.m"; sourceTree = ""; }; BF1E315022A0616100370A3C /* libAltKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libAltKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; + BF3F786322CAA41E008FBD20 /* ALTDeviceManager+Installation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTDeviceManager+Installation.swift"; sourceTree = ""; }; BF43002D22A714AF0051E2BC /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+AltStore.swift"; sourceTree = ""; }; 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 = ""; }; - BF458691229872EA00BD7491 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; BF458693229872EA00BD7491 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; BF458696229872EA00BD7491 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; BF458698229872EA00BD7491 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -469,7 +469,6 @@ children = ( BF45868F229872EA00BD7491 /* AppDelegate.swift */, BF458695229872EA00BD7491 /* Main.storyboard */, - BF458691229872EA00BD7491 /* ViewController.swift */, BF703195229F36FF006E110F /* Devices */, BFD52BDC22A0A659000B7ED1 /* Connections */, BF703194229F36F6006E110F /* Resources */, @@ -630,6 +629,7 @@ children = ( BF4586C32298CDB800BD7491 /* ALTDeviceManager.h */, BF4586C42298CDB800BD7491 /* ALTDeviceManager.mm */, + BF3F786322CAA41E008FBD20 /* ALTDeviceManager+Installation.swift */, ); path = Devices; sourceTree = ""; @@ -1121,7 +1121,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - BF458692229872EA00BD7491 /* ViewController.swift in Sources */, + BF3F786422CAA41E008FBD20 /* ALTDeviceManager+Installation.swift in Sources */, BF1E312B229F474900370A3C /* ConnectionManager.swift in Sources */, BF458690229872EA00BD7491 /* AppDelegate.swift in Sources */, BF4586C52298CDB800BD7491 /* ALTDeviceManager.mm in Sources */, diff --git a/Dependencies/libimobiledevice b/Dependencies/libimobiledevice index 7dc84af1..df4f2b0a 160000 --- a/Dependencies/libimobiledevice +++ b/Dependencies/libimobiledevice @@ -1 +1 @@ -Subproject commit 7dc84af1fbb54e5a591d60ed5fc4b8279d2bf39d +Subproject commit df4f2b0ac56479f8952e8159c9c64ab68a935301