mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
Merge pull request #38 from hugeBlack/main
fetch icon from springboard services
This commit is contained in:
commit
d000df4a08
12 changed files with 128 additions and 62 deletions
|
|
@ -51,10 +51,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
|
||||
|
|
@ -357,7 +353,7 @@ func startHeartbeatInBackground() {
|
|||
}
|
||||
}
|
||||
|
||||
JITEnableContext.shared().startHeartbeat(completionHandler: completionHandler, logger: nil)
|
||||
JITEnableContext.shared.startHeartbeat(completionHandler: completionHandler, logger: nil)
|
||||
}
|
||||
|
||||
heartBeat.qualityOfService = .background
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ extension UIDocumentPickerViewController {
|
|||
struct HomeView: View {
|
||||
@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 = ""
|
||||
|
|
@ -264,7 +265,7 @@ struct HomeView: View {
|
|||
LogManager.shared.addInfoLog("Starting JIT for \(bundleID)")
|
||||
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
JITEnableContext.shared().debugApp(withBundleID: bundleID, logger: { message in
|
||||
let success = JITEnableContext.shared.debugApp(withBundleID: bundleID, logger: { message in
|
||||
if let message = message {
|
||||
// Log messages from the JIT process
|
||||
LogManager.shared.addInfoLog(message)
|
||||
|
|
@ -274,6 +275,10 @@ struct HomeView: View {
|
|||
DispatchQueue.main.async {
|
||||
LogManager.shared.addInfoLog("JIT process completed for \(bundleID)")
|
||||
isProcessing = false
|
||||
|
||||
if success && doAutoQuitAfterEnablingJIT {
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -288,7 +293,7 @@ class InstalledAppsViewModel: ObservableObject {
|
|||
|
||||
func loadApps() {
|
||||
do {
|
||||
self.apps = try JITEnableContext.shared().getAppList()
|
||||
self.apps = try JITEnableContext.shared.getAppList()
|
||||
} catch {
|
||||
print(error)
|
||||
self.apps = [:]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -32,7 +33,7 @@ struct SettingsView: View {
|
|||
"Stossy11": "https://github.com/Stossy11.png",
|
||||
"Neo": "https://github.com/neoarz.png",
|
||||
"Se2crid": "https://github.com/Se2crid.png",
|
||||
"HugeBlack": "https://github.com/HugeBlack.png"
|
||||
"Huge_Black": "https://github.com/HugeBlack.png"
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -95,6 +96,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) {
|
||||
|
|
|
|||
|
|
@ -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 logger:(LogFunc)logger;
|
||||
- (BOOL)debugAppWithBundleID:(NSString*)bundleID logger:(LogFunc)logger;
|
||||
- (NSDictionary<NSString*, NSString*>*)getAppListWithError:(NSError**)error;
|
||||
- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error;
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -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,16 @@ JITEnableContext* sharedJITContext = nil;
|
|||
completionHandler(result,[NSString stringWithCString:message encoding:NSASCIIStringEncoding]);
|
||||
}, [self createCLogger:logger]);
|
||||
}
|
||||
- (void)debugAppWithBundleID:(NSString*)bundleID logger:(LogFunc)logger {
|
||||
- (BOOL)debugAppWithBundleID:(NSString*)bundleID 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]);
|
||||
return debug_app(provider, [bundleID UTF8String], [self createCLogger:logger]) == 0;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -127,6 +135,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) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
#ifndef APPLIST_H
|
||||
#define APPLIST_H
|
||||
@import Foundation;
|
||||
@import UIKit;
|
||||
|
||||
NSDictionary<NSString*, NSString*>* list_installed_apps(TcpProviderHandle* provider, NSString** error);
|
||||
UIImage* getAppIcon(TcpProviderHandle* provider, NSString* bundleID, NSString** error);
|
||||
|
||||
#endif /* APPLIST_H */
|
||||
|
|
|
|||
|
|
@ -84,3 +84,30 @@ NSDictionary<NSString*, NSString*>* 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;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
// Jackson Coxson
|
||||
|
||||
#include "idevice.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue