diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index 8edbe4e..367a508 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -53,10 +53,6 @@ struct HeartbeatApp: App { } return } - var log_path = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path.utf8CString - log_path.withUnsafeMutableBufferPointer { buffer in - idevice_init_logger(Debug, Debug, buffer.baseAddress) - } startProxy() { result, error in if result { checkVPNConnection() { result, vpn_error in @@ -371,7 +367,7 @@ func startHeartbeatInBackground() { } } - JITEnableContext.shared().startHeartbeat(completionHandler: completionHandler, logger: nil) + JITEnableContext.shared.startHeartbeat(completionHandler: completionHandler, logger: nil) } heartBeat.qualityOfService = .background diff --git a/StikJIT/Utilities/AppStoreIconFetcher.swift b/StikJIT/Utilities/AppStoreIconFetcher.swift index 3ca012e..c093386 100644 --- a/StikJIT/Utilities/AppStoreIconFetcher.swift +++ b/StikJIT/Utilities/AppStoreIconFetcher.swift @@ -7,8 +7,10 @@ import UIKit +// Not AppStore class AppStoreIconFetcher { static private var iconCache: [String: UIImage] = [:] + static private let iconFetchDispatchQueue = DispatchQueue(label: "com.stik.StikJIT.iconFetchQueue", attributes: .concurrent) static func getIcon(for bundleID: String, completion: @escaping (UIImage?) -> Void) { // Check cache first @@ -17,55 +19,15 @@ class AppStoreIconFetcher { return } - // Hit the App Store API - let baseURLString = "https://itunes.apple.com/lookup?bundleId=" - let urlString = baseURLString + bundleID - - guard let url = URL(string: urlString) else { - DispatchQueue.main.async { completion(nil) } - return - } - - URLSession.shared.dataTask(with: url) { data, response, error in - guard let data = data, error == nil else { - DispatchQueue.main.async { completion(nil) } - return - } - + iconFetchDispatchQueue.sync { do { - if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], - let results = json["results"] as? [[String: Any]], - !results.isEmpty, - let firstApp = results.first, - let iconURLString = firstApp["artworkUrl512"] as? String ?? firstApp["artworkUrl100"] as? String, - let iconURL = URL(string: iconURLString) { - - // Download the icon image - downloadImage(from: iconURL) { image in - if let image = image { - // Cache the icon - iconCache[bundleID] = image - } - DispatchQueue.main.async { - completion(image) - } - } - } else { - DispatchQueue.main.async { completion(nil) } - } + let ans = try JITEnableContext.shared.getAppIcon(withBundleId: bundleID) + iconCache[bundleID] = ans + completion(ans) } catch { - DispatchQueue.main.async { completion(nil) } - } - }.resume() - } - - private static func downloadImage(from url: URL, completion: @escaping (UIImage?) -> Void) { - URLSession.shared.dataTask(with: url) { data, response, error in - if let data = data, let image = UIImage(data: data) { - completion(image) - } else { + print("Failed to get icon: \(error)") completion(nil) } - }.resume() + } } -} \ No newline at end of file +} diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index f01dd00..ce6ad27 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -18,6 +18,7 @@ struct HomeView: View { @Binding var is_lc: Bool @AppStorage("username") private var username = "User" @AppStorage("customBackgroundColor") private var customBackgroundColorHex: String = Color.primaryBackground.toHex() ?? "#000000" + @AppStorage("autoQuitAfterEnablingJIT") private var doAutoQuitAfterEnablingJIT = false @State private var selectedBackgroundColor: Color = Color(hex: UserDefaults.standard.string(forKey: "customBackgroundColor") ?? "#000000") ?? Color.primaryBackground let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @AppStorage("bundleID") private var bundleID: String = "" @@ -269,7 +270,9 @@ struct HomeView: View { LogManager.shared.addInfoLog("Starting JIT for \(bundleID)") DispatchQueue.global(qos: .background).async { + JITEnableContext.shared().debugApp(withBundleID: bundleID,isLC: is_lc, logger: { message in + if let message = message { // Log messages from the JIT process LogManager.shared.addInfoLog(message) @@ -279,6 +282,10 @@ struct HomeView: View { DispatchQueue.main.async { LogManager.shared.addInfoLog("JIT process completed for \(bundleID)") isProcessing = false + + if success && doAutoQuitAfterEnablingJIT { + exit(0) + } } } } @@ -293,7 +300,7 @@ class InstalledAppsViewModel: ObservableObject { func loadApps() { do { - self.apps = try JITEnableContext.shared().getAppList() + self.apps = try JITEnableContext.shared.getAppList() } catch { print(error) self.apps = [:] diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 0c75058..3cdbc8b 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -10,6 +10,7 @@ struct SettingsView: View { @AppStorage("username") private var username = "User" @AppStorage("customBackgroundColor") private var customBackgroundColorHex: String = Color.primaryBackground.toHex() ?? "#000000" @AppStorage("selectedAppIcon") private var selectedAppIcon: String = "AppIcon" + @AppStorage("autoQuitAfterEnablingJIT") private var doAutoQuitAfterEnablingJIT = false @State private var isShowingPairingFilePicker = false @State private var selectedBackgroundColor: Color = Color.primaryBackground @@ -97,6 +98,21 @@ struct SettingsView: View { .padding(.horizontal, 16) } + SettingsCard { + VStack(alignment: .leading, spacing: 20) { + Text("Behavior") + .font(.headline) + .foregroundColor(.primary) + .padding(.bottom, 4) + + Toggle("Automatically Quit After Enabling JIT", isOn: $doAutoQuitAfterEnablingJIT) + .foregroundColor(.primary) + .padding(.vertical, 6) + } + .padding(.vertical, 20) + .padding(.horizontal, 16) + } + // Pairing File section SettingsCard { VStack(alignment: .leading, spacing: 20) { diff --git a/StikJIT/idevice/JITEnableContext.h b/StikJIT/idevice/JITEnableContext.h index 4e0fd3a..5928dc6 100644 --- a/StikJIT/idevice/JITEnableContext.h +++ b/StikJIT/idevice/JITEnableContext.h @@ -5,6 +5,7 @@ // Created by s s on 2025/3/28. // @import Foundation; +@import UIKit; #include "idevice.h" typedef void (^HeartbeatCompletionHandler)(int result, NSString *message); @@ -12,9 +13,10 @@ typedef void (^LogFuncC)(const char* message, ...); typedef void (^LogFunc)(NSString *message); @interface JITEnableContext : NSObject -+ (instancetype)shared; +@property (class, readonly)JITEnableContext* shared; - (IdevicePairingFile*)getPairingFileWithError:(NSError**)error; - (void)startHeartbeatWithCompletionHandler:(HeartbeatCompletionHandler)completionHandler logger:(LogFunc)logger; - (void)debugAppWithBundleID:(NSString*)bundleID isLC:(BOOL)isLC logger:(LogFunc)logger; - (NSDictionary*)getAppListWithError:(NSError**)error; +- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error; @end diff --git a/StikJIT/idevice/JITEnableContext.m b/StikJIT/idevice/JITEnableContext.m index 4777402..330812d 100644 --- a/StikJIT/idevice/JITEnableContext.m +++ b/StikJIT/idevice/JITEnableContext.m @@ -29,6 +29,14 @@ JITEnableContext* sharedJITContext = nil; return sharedJITContext; } +- (instancetype)init { + NSFileManager* fm = [NSFileManager defaultManager]; + NSURL* docPathUrl = [fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject; + NSURL* pairingFileURL = [docPathUrl URLByAppendingPathComponent:@"idevice_log.txt"]; + idevice_init_logger(Debug, Debug, (char*)pairingFileURL.path.UTF8String); + return self; +} + - (NSError*)errorWithStr:(NSString*)str code:(int)code { return [NSError errorWithDomain:@"StikJIT" code:code userInfo:@{NSLocalizedDescriptionKey: str}]; } @@ -96,16 +104,21 @@ JITEnableContext* sharedJITContext = nil; completionHandler(result,[NSString stringWithCString:message encoding:NSASCIIStringEncoding]); }, [self createCLogger:logger]); } + - (void)debugAppWithBundleID:(NSString*)bundleID isLC:(BOOL)isLC logger:(LogFunc)logger { + if(!provider) { if(logger) { logger(@"Provider not initialized!"); } NSLog(@"Provider not initialized!"); - return; + return NO; } - debug_app(provider, [bundleID UTF8String], [self createCLogger:logger], (bool)(isLC)); + + + return debug_app(provider, [bundleID UTF8String], [self createCLogger:logger], (bool)(isLC)) == 0; + } @@ -127,6 +140,23 @@ JITEnableContext* sharedJITContext = nil; } } +- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error { + if(!provider) { + NSLog(@"Provider not initialized!"); + *error = [self errorWithStr:@"Provider not initialized!" code:-1]; + return nil; + } + + NSString* errorStr = nil; + UIImage* ans = getAppIcon(provider, bundleId, &errorStr); + if(errorStr){ + *error = [self errorWithStr:errorStr code:-17]; + return nil; + } else { + return ans; + } +} + - (void)dealloc { self->heartbeatSessionId = arc4random(); if(provider) { diff --git a/StikJIT/idevice/applist.h b/StikJIT/idevice/applist.h index 13818d4..153b1d3 100644 --- a/StikJIT/idevice/applist.h +++ b/StikJIT/idevice/applist.h @@ -8,7 +8,9 @@ #ifndef APPLIST_H #define APPLIST_H @import Foundation; +@import UIKit; NSDictionary* list_installed_apps(TcpProviderHandle* provider, NSString** error); +UIImage* getAppIcon(TcpProviderHandle* provider, NSString* bundleID, NSString** error); #endif /* APPLIST_H */ diff --git a/StikJIT/idevice/applist.m b/StikJIT/idevice/applist.m index 43c8af1..5d6e355 100644 --- a/StikJIT/idevice/applist.m +++ b/StikJIT/idevice/applist.m @@ -84,3 +84,30 @@ NSDictionary* list_installed_apps(TcpProviderHandle* provi return ans; } + + +UIImage* getAppIcon(TcpProviderHandle* provider, NSString* bundleID, NSString** error) { + IdeviceErrorCode err = IdeviceSuccess; + + SpringBoardServicesClientHandle *client = NULL; + springboard_services_connect_tcp(provider, &client); + if (err != IdeviceSuccess) { + *error = @"Failed to connect to SpringBoard Services"; + return nil; + } + + void *pngData = NULL; + size_t data_len = 0; + err = springboard_services_get_icon(client, [bundleID UTF8String], &pngData, &data_len); + if (err != IdeviceSuccess) { + springboard_services_free(client); + *error = @"Failed to get app icon"; + return nil; + } + + NSData* pngNSData = [NSData dataWithBytes:pngData length:data_len]; + free(pngData); + UIImage* ans = [UIImage imageWithData:pngNSData]; + return ans; + +} diff --git a/StikJIT/idevice/heartbeat.c b/StikJIT/idevice/heartbeat.c index 413c067..9e8228c 100644 --- a/StikJIT/idevice/heartbeat.c +++ b/StikJIT/idevice/heartbeat.c @@ -13,7 +13,6 @@ void startHeartbeat(IdevicePairingFile* pairing_file, TcpProviderHandle** provider, int* heartbeatSessionId, HeartbeatCompletionHandlerC completion, LogFuncC logger) { int currentSessionId = *heartbeatSessionId; - logger("DEBUG: Initializing logger..."); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); diff --git a/StikJIT/idevice/idevice.h b/StikJIT/idevice/idevice.h index 92a4e92..6dab1c6 100644 --- a/StikJIT/idevice/idevice.h +++ b/StikJIT/idevice/idevice.h @@ -116,6 +116,8 @@ typedef struct ProcessControlAdapterHandle ProcessControlAdapterHandle; */ typedef struct RemoteServerAdapterHandle RemoteServerAdapterHandle; +typedef struct SpringBoardServicesClientHandle SpringBoardServicesClientHandle; + typedef struct TcpProviderHandle TcpProviderHandle; typedef struct UsbmuxdAddrHandle UsbmuxdAddrHandle; @@ -2009,4 +2011,35 @@ enum IdeviceErrorCode idevice_usbmuxd_unix_addr_new(const char *addr, */ void idevice_usbmuxd_addr_free(struct UsbmuxdAddrHandle *usbmuxd_addr); +enum IdeviceErrorCode springboard_services_connect_tcp(struct TcpProviderHandle *provider, + struct SpringBoardServicesClientHandle **client); + +enum IdeviceErrorCode springboard_services_connect_usbmuxd(struct UsbmuxdProviderHandle *provider, + struct SpringBoardServicesClientHandle **client); + +enum IdeviceErrorCode springboard_services_new(struct IdeviceHandle *socket, + struct SpringBoardServicesClientHandle **client); + +/** + * Gets the icon of the specified app by bundle identifier + * + * # Arguments + * * `client` - A valid SpringBoardServicesClient handle + * * `bundle_identifier` - The identifiers of the app to get icon + * * `out_result` - On success, will be set to point to a newly allocated png data + * + * # Returns + * An error code indicating success or failure + * + * # Safety + * `client` must be a valid pointer to a handle allocated by this library + * `out_result` must be a valid, non-null pointer to a location where the result will be stored + */ +enum IdeviceErrorCode springboard_services_get_icon(struct SpringBoardServicesClientHandle *client, + const char *bundle_identifier, + void **out_result, + size_t *out_result_len); + +void springboard_services_free(struct SpringBoardServicesClientHandle *handle); + #endif diff --git a/StikJIT/idevice/jit.c b/StikJIT/idevice/jit.c index 1acc41d..c7cf0ce 100644 --- a/StikJIT/idevice/jit.c +++ b/StikJIT/idevice/jit.c @@ -7,7 +7,6 @@ // Jackson Coxson -#include "idevice.h" #include #include #include diff --git a/StikJIT/idevice/libidevice_ffi.a b/StikJIT/idevice/libidevice_ffi.a index 50d4755..594e0ef 100644 Binary files a/StikJIT/idevice/libidevice_ffi.a and b/StikJIT/idevice/libidevice_ffi.a differ