diff --git a/RootHelper/.theos/obj/debug/arm64/trolltoolsroothelper b/RootHelper/.theos/obj/debug/arm64/trolltoolsroothelper index 92a568d..7fde8fa 100755 Binary files a/RootHelper/.theos/obj/debug/arm64/trolltoolsroothelper and b/RootHelper/.theos/obj/debug/arm64/trolltoolsroothelper differ diff --git a/RootHelper/.theos/obj/debug/arm64/trolltoolsroothelper.dSYM/Contents/Resources/DWARF/trolltoolsroothelper b/RootHelper/.theos/obj/debug/arm64/trolltoolsroothelper.dSYM/Contents/Resources/DWARF/trolltoolsroothelper index 0da35db..b02046b 100644 Binary files a/RootHelper/.theos/obj/debug/arm64/trolltoolsroothelper.dSYM/Contents/Resources/DWARF/trolltoolsroothelper and b/RootHelper/.theos/obj/debug/arm64/trolltoolsroothelper.dSYM/Contents/Resources/DWARF/trolltoolsroothelper differ diff --git a/RootHelper/.theos/obj/debug/trolltoolsroothelper b/RootHelper/.theos/obj/debug/trolltoolsroothelper index a5f7f26..31f03d8 100755 Binary files a/RootHelper/.theos/obj/debug/trolltoolsroothelper and b/RootHelper/.theos/obj/debug/trolltoolsroothelper differ diff --git a/RootHelper/CoreServices.h b/RootHelper/CoreServices.h new file mode 100644 index 0000000..e80af10 --- /dev/null +++ b/RootHelper/CoreServices.h @@ -0,0 +1,58 @@ +@interface LSBundleProxy +@property (nonatomic,readonly) NSString * bundleIdentifier; +@property (nonatomic) NSURL* dataContainerURL; +-(NSString*)localizedName; +@end + +@interface LSApplicationProxy : LSBundleProxy ++ (instancetype)applicationProxyForIdentifier:(NSString*)identifier; +@property NSURL* bundleURL; +@property NSString* bundleType; +@property NSString* canonicalExecutablePath; +@property (nonatomic,readonly) NSDictionary* groupContainerURLs; +@property (nonatomic,readonly) NSArray* plugInKitPlugins; +@property (getter=isInstalled,nonatomic,readonly) BOOL installed; +@property (getter=isPlaceholder,nonatomic,readonly) BOOL placeholder; +@property (getter=isRestricted,nonatomic,readonly) BOOL restricted; +@property (nonatomic,readonly) NSSet * claimedURLSchemes; +@end + +@interface LSApplicationWorkspace : NSObject ++ (instancetype)defaultWorkspace; +- (BOOL)registerApplicationDictionary:(NSDictionary*)dict; +- (BOOL)unregisterApplication:(id)arg1; +- (BOOL)_LSPrivateRebuildApplicationDatabasesForSystemApps:(BOOL)arg1 internal:(BOOL)arg2 user:(BOOL)arg3; +- (BOOL)uninstallApplication:(NSString*)arg1 withOptions:(id)arg2; +- (BOOL)openApplicationWithBundleID:(NSString *)arg1 ; +- (void)enumerateApplicationsOfType:(NSUInteger)type block:(void (^)(LSApplicationProxy*))block; +@end + +@interface LSEnumerator : NSEnumerator +@property (nonatomic,copy) NSPredicate * predicate; ++ (instancetype)enumeratorForApplicationProxiesWithOptions:(NSUInteger)options; +@end + +@interface LSPlugInKitProxy : LSBundleProxy +@property (nonatomic,readonly) NSString* pluginIdentifier; +@property (nonatomic,readonly) NSDictionary * pluginKitDictionary; ++ (instancetype)pluginKitProxyForIdentifier:(NSString*)arg1; +@end + +@interface MCMContainer : NSObject ++ (id)containerWithIdentifier:(id)arg1 createIfNecessary:(BOOL)arg2 existed:(BOOL*)arg3 error:(id*)arg4; +@property (nonatomic,readonly) NSURL * url; +@end + +@interface MCMDataContainer : MCMContainer + +@end + +@interface MCMAppDataContainer : MCMDataContainer + +@end + +@interface MCMAppContainer : MCMContainer +@end + +@interface MCMPluginKitPluginDataContainer : MCMDataContainer +@end \ No newline at end of file diff --git a/RootHelper/RemoteLog.h b/RootHelper/RemoteLog.h new file mode 100644 index 0000000..70835d6 --- /dev/null +++ b/RootHelper/RemoteLog.h @@ -0,0 +1,57 @@ +#ifndef _REMOTE_LOG_H_ +#define _REMOTE_LOG_H_ + +#import +#import +#import +#import + +// change this to match your destination (server) IP address +#define RLOG_IP_ADDRESS "192.168.0.24" +#define RLOG_PORT 11909 + +__attribute__((unused)) static void RLogv(NSString* format, va_list args) +{ + NSString* str = [[NSString alloc] initWithFormat:format arguments:args]; + + int sd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sd <= 0) + { + NSLog(@"[RemoteLog] Error: Could not open socket"); + return; + } + + int broadcastEnable = 1; + int ret = setsockopt(sd, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, sizeof(broadcastEnable)); + if (ret) + { + NSLog(@"[RemoteLog] Error: Could not open set socket to broadcast mode"); + close(sd); + return; + } + + struct sockaddr_in broadcastAddr; + memset(&broadcastAddr, 0, sizeof broadcastAddr); + broadcastAddr.sin_family = AF_INET; + inet_pton(AF_INET, RLOG_IP_ADDRESS, &broadcastAddr.sin_addr); + broadcastAddr.sin_port = htons(RLOG_PORT); + + char* request = (char*)[str UTF8String]; + ret = sendto(sd, request, strlen(request), 0, (struct sockaddr*)&broadcastAddr, sizeof broadcastAddr); + if (ret < 0) + { + NSLog(@"[RemoteLog] Error: Could not send broadcast"); + close(sd); + return; + } + close(sd); +} + +__attribute__((unused)) static void RLog(NSString* format, ...) +{ + va_list args; + va_start(args, format); + RLogv(format, args); + va_end(args); +} +#endif diff --git a/RootHelper/TSUtil.h b/RootHelper/TSUtil.h new file mode 100644 index 0000000..1fc0bcc --- /dev/null +++ b/RootHelper/TSUtil.h @@ -0,0 +1,61 @@ +@import Foundation; +#import "CoreServices.h" + +#define TrollStoreErrorDomain @"TrollStoreErrorDomain" + +extern void chineseWifiFixup(void); +extern void loadMCMFramework(void); +extern NSString* safe_getExecutablePath(); +extern NSString* rootHelperPath(void); +extern NSString* getNSStringFromFile(int fd); +extern void printMultilineNSString(NSString* stringToPrint); +extern int spawnRoot(NSString* path, NSArray* args, NSString** stdOut, NSString** stdErr); +extern void killall(NSString* processName); +extern void respring(void); +extern void fetchLatestTrollStoreVersion(void (^completionHandler)(NSString* latestVersion)); + +extern NSArray* trollStoreInstalledAppBundlePaths(); +extern NSArray* trollStoreInstalledAppContainerPaths(); +extern NSString* trollStorePath(); +extern NSString* trollStoreAppPath(); + +#import + +@interface UIAlertController (Private) +@property (setter=_setAttributedTitle:,getter=_attributedTitle,nonatomic,copy) NSAttributedString* attributedTitle; +@property (setter=_setAttributedMessage:,getter=_attributedMessage,nonatomic,copy) NSAttributedString* attributedMessage; +@property (nonatomic,retain) UIImage* image; +@end + +typedef enum +{ + PERSISTENCE_HELPER_TYPE_USER = 1 << 0, + PERSISTENCE_HELPER_TYPE_SYSTEM = 1 << 1, + PERSISTENCE_HELPER_TYPE_ALL = PERSISTENCE_HELPER_TYPE_USER | PERSISTENCE_HELPER_TYPE_SYSTEM +} PERSISTENCE_HELPER_TYPE; + +extern LSApplicationProxy* findPersistenceHelperApp(PERSISTENCE_HELPER_TYPE allowedTypes); + +typedef struct __SecCode const *SecStaticCodeRef; + +typedef CF_OPTIONS(uint32_t, SecCSFlags) { + kSecCSDefaultFlags = 0 +}; +#define kSecCSRequirementInformation 1 << 2 +#define kSecCSSigningInformation 1 << 1 + +OSStatus SecStaticCodeCreateWithPathAndAttributes(CFURLRef path, SecCSFlags flags, CFDictionaryRef attributes, SecStaticCodeRef *staticCode); +OSStatus SecCodeCopySigningInformation(SecStaticCodeRef code, SecCSFlags flags, CFDictionaryRef *information); +CFDataRef SecCertificateCopyExtensionValue(SecCertificateRef certificate, CFTypeRef extensionOID, bool *isCritical); +void SecPolicySetOptionsValue(SecPolicyRef policy, CFStringRef key, CFTypeRef value); + +extern CFStringRef kSecCodeInfoEntitlementsDict; +extern CFStringRef kSecCodeInfoCertificates; +extern CFStringRef kSecPolicyAppleiPhoneApplicationSigning; +extern CFStringRef kSecPolicyAppleiPhoneProfileApplicationSigning; +extern CFStringRef kSecPolicyLeafMarkerOid; + +extern SecStaticCodeRef getStaticCodeRef(NSString *binaryPath); +extern NSDictionary* dumpEntitlements(SecStaticCodeRef codeRef); +extern NSDictionary* dumpEntitlementsFromBinaryAtPath(NSString *binaryPath); +extern NSDictionary* dumpEntitlementsFromBinaryData(NSData* binaryData); \ No newline at end of file diff --git a/RootHelper/TSUtil.m b/RootHelper/TSUtil.m new file mode 100644 index 0000000..a6617e4 --- /dev/null +++ b/RootHelper/TSUtil.m @@ -0,0 +1,450 @@ +#import "TSUtil.h" + +#import +#import +#import + +@interface PSAppDataUsagePolicyCache : NSObject ++ (instancetype)sharedInstance; +- (void)setUsagePoliciesForBundle:(NSString*)bundleId cellular:(BOOL)cellular wifi:(BOOL)wifi; +@end + +#define POSIX_SPAWN_PERSONA_FLAGS_OVERRIDE 1 +extern int posix_spawnattr_set_persona_np(const posix_spawnattr_t* __restrict, uid_t, uint32_t); +extern int posix_spawnattr_set_persona_uid_np(const posix_spawnattr_t* __restrict, uid_t); +extern int posix_spawnattr_set_persona_gid_np(const posix_spawnattr_t* __restrict, uid_t); + +void chineseWifiFixup(void) +{ + NSBundle *bundle = [NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/SettingsCellular.framework"]; + [bundle load]; + [[NSClassFromString(@"PSAppDataUsagePolicyCache") sharedInstance] setUsagePoliciesForBundle:NSBundle.mainBundle.bundleIdentifier cellular:true wifi:true]; +} + +void loadMCMFramework(void) +{ + static dispatch_once_t onceToken; + dispatch_once (&onceToken, ^{ + NSBundle* mcmBundle = [NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/MobileContainerManager.framework"]; + [mcmBundle load]; + }); +} + +extern char*** _NSGetArgv(); +NSString* safe_getExecutablePath() +{ + char* executablePathC = **_NSGetArgv(); + return [NSString stringWithUTF8String:executablePathC]; +} + +#ifdef EMBEDDED_ROOT_HELPER +NSString* rootHelperPath(void) +{ + return safe_getExecutablePath(); +} +#else +NSString* rootHelperPath(void) +{ + return [[NSBundle mainBundle].bundlePath stringByAppendingPathComponent:@"trollstorehelper"]; +} +#endif + +NSString* getNSStringFromFile(int fd) +{ + NSMutableString* ms = [NSMutableString new]; + ssize_t num_read; + char c; + while((num_read = read(fd, &c, sizeof(c)))) + { + [ms appendString:[NSString stringWithFormat:@"%c", c]]; + } + return ms.copy; +} + +void printMultilineNSString(NSString* stringToPrint) +{ + NSCharacterSet *separator = [NSCharacterSet newlineCharacterSet]; + NSArray* lines = [stringToPrint componentsSeparatedByCharactersInSet:separator]; + for(NSString* line in lines) + { + NSLog(@"%@", line); + } +} + +int spawnRoot(NSString* path, NSArray* args, NSString** stdOut, NSString** stdErr) +{ + NSMutableArray* argsM = args.mutableCopy ?: [NSMutableArray new]; + [argsM insertObject:path.lastPathComponent atIndex:0]; + + NSUInteger argCount = [argsM count]; + char **argsC = (char **)malloc((argCount + 1) * sizeof(char*)); + + for (NSUInteger i = 0; i < argCount; i++) + { + argsC[i] = strdup([[argsM objectAtIndex:i] UTF8String]); + } + argsC[argCount] = NULL; + + posix_spawnattr_t attr; + posix_spawnattr_init(&attr); + + posix_spawnattr_set_persona_np(&attr, 99, POSIX_SPAWN_PERSONA_FLAGS_OVERRIDE); + posix_spawnattr_set_persona_uid_np(&attr, 0); + posix_spawnattr_set_persona_gid_np(&attr, 0); + + posix_spawn_file_actions_t action; + posix_spawn_file_actions_init(&action); + + int outErr[2]; + if(stdErr) + { + pipe(outErr); + posix_spawn_file_actions_adddup2(&action, outErr[1], STDERR_FILENO); + posix_spawn_file_actions_addclose(&action, outErr[0]); + } + + int out[2]; + if(stdOut) + { + pipe(out); + posix_spawn_file_actions_adddup2(&action, out[1], STDOUT_FILENO); + posix_spawn_file_actions_addclose(&action, out[0]); + } + + pid_t task_pid; + int status = -200; + int spawnError = posix_spawn(&task_pid, [path UTF8String], &action, &attr, (char* const*)argsC, NULL); + posix_spawnattr_destroy(&attr); + for (NSUInteger i = 0; i < argCount; i++) + { + free(argsC[i]); + } + free(argsC); + + if(spawnError != 0) + { + NSLog(@"posix_spawn error %d\n", spawnError); + return spawnError; + } + + do + { + if (waitpid(task_pid, &status, 0) != -1) { + NSLog(@"Child status %d", WEXITSTATUS(status)); + } else + { + perror("waitpid"); + return -222; + } + } while (!WIFEXITED(status) && !WIFSIGNALED(status)); + + if(stdOut) + { + close(out[1]); + NSString* output = getNSStringFromFile(out[0]); + *stdOut = output; + } + + if(stdErr) + { + close(outErr[1]); + NSString* errorOutput = getNSStringFromFile(outErr[0]); + *stdErr = errorOutput; + } + + return WEXITSTATUS(status); +} + +void enumerateProcessesUsingBlock(void (^enumerator)(pid_t pid, NSString* executablePath, BOOL* stop)) +{ + static int maxArgumentSize = 0; + if (maxArgumentSize == 0) { + size_t size = sizeof(maxArgumentSize); + if (sysctl((int[]){ CTL_KERN, KERN_ARGMAX }, 2, &maxArgumentSize, &size, NULL, 0) == -1) { + perror("sysctl argument size"); + maxArgumentSize = 4096; // Default + } + } + int mib[3] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL}; + struct kinfo_proc *info; + size_t length; + int count; + + if (sysctl(mib, 3, NULL, &length, NULL, 0) < 0) + return; + if (!(info = malloc(length))) + return; + if (sysctl(mib, 3, info, &length, NULL, 0) < 0) { + free(info); + return; + } + count = length / sizeof(struct kinfo_proc); + for (int i = 0; i < count; i++) { + @autoreleasepool { + pid_t pid = info[i].kp_proc.p_pid; + if (pid == 0) { + continue; + } + size_t size = maxArgumentSize; + char* buffer = (char *)malloc(length); + if (sysctl((int[]){ CTL_KERN, KERN_PROCARGS2, pid }, 3, buffer, &size, NULL, 0) == 0) { + NSString* executablePath = [NSString stringWithCString:(buffer+sizeof(int)) encoding:NSUTF8StringEncoding]; + + BOOL stop = NO; + enumerator(pid, executablePath, &stop); + if(stop) + { + free(buffer); + break; + } + } + free(buffer); + } + } + free(info); +} + +void killall(NSString* processName) +{ + enumerateProcessesUsingBlock(^(pid_t pid, NSString* executablePath, BOOL* stop) + { + if([executablePath.lastPathComponent isEqualToString:processName]) + { + kill(pid, SIGTERM); + } + }); +} + +void respring(void) +{ + killall(@"SpringBoard"); + exit(0); +} + +void fetchLatestTrollStoreVersion(void (^completionHandler)(NSString* latestVersion)) +{ + NSURL* githubLatestAPIURL = [NSURL URLWithString:@"https://api.github.com/repos/opa334/TrollStore/releases/latest"]; + + NSURLSessionDataTask* task = [NSURLSession.sharedSession dataTaskWithURL:githubLatestAPIURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) + { + if(!error) + { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) + { + NSError *jsonError; + NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + + if (!jsonError) + { + completionHandler(jsonResponse[@"tag_name"]); + } + } + } + }]; + + [task resume]; +} + +NSArray* trollStoreInstalledAppContainerPaths() +{ + NSMutableArray* appContainerPaths = [NSMutableArray new]; + + NSString* appContainersPath = @"/var/containers/Bundle/Application"; + + NSError* error; + NSArray* containers = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:appContainersPath error:&error]; + if(error) + { + NSLog(@"error getting app bundles paths %@", error); + } + if(!containers) return nil; + + for(NSString* container in containers) + { + NSString* containerPath = [appContainersPath stringByAppendingPathComponent:container]; + BOOL isDirectory = NO; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:containerPath isDirectory:&isDirectory]; + if(exists && isDirectory) + { + NSString* trollStoreMark = [containerPath stringByAppendingPathComponent:@"_TrollStore"]; + if([[NSFileManager defaultManager] fileExistsAtPath:trollStoreMark]) + { + NSString* trollStoreApp = [containerPath stringByAppendingPathComponent:@"TrollStore.app"]; + if(![[NSFileManager defaultManager] fileExistsAtPath:trollStoreApp]) + { + [appContainerPaths addObject:containerPath]; + } + } + } + } + + return appContainerPaths.copy; +} + +NSArray* trollStoreInstalledAppBundlePaths() +{ + NSMutableArray* appPaths = [NSMutableArray new]; + for(NSString* containerPath in trollStoreInstalledAppContainerPaths()) + { + NSArray* items = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:containerPath error:nil]; + if(!items) return nil; + + for(NSString* item in items) + { + if([item.pathExtension isEqualToString:@"app"]) + { + [appPaths addObject:[containerPath stringByAppendingPathComponent:item]]; + } + } + } + return appPaths.copy; +} + +NSString* trollStorePath() +{ + loadMCMFramework(); + NSError* mcmError; + MCMAppContainer* appContainer = [NSClassFromString(@"MCMAppContainer") containerWithIdentifier:@"com.opa334.TrollStore" createIfNecessary:NO existed:NULL error:&mcmError]; + if(!appContainer) return nil; + return appContainer.url.path; +} + +NSString* trollStoreAppPath() +{ + return [trollStorePath() stringByAppendingPathComponent:@"TrollStore.app"]; +} + +LSApplicationProxy* findPersistenceHelperApp(PERSISTENCE_HELPER_TYPE allowedTypes) +{ + __block LSApplicationProxy* outProxy; + + void (^searchBlock)(LSApplicationProxy* appProxy) = ^(LSApplicationProxy* appProxy) + { + if(appProxy.installed && !appProxy.restricted) + { + if([appProxy.bundleURL.path hasPrefix:@"/private/var/containers"]) + { + NSURL* trollStorePersistenceMarkURL = [appProxy.bundleURL URLByAppendingPathComponent:@".TrollStorePersistenceHelper"]; + if([trollStorePersistenceMarkURL checkResourceIsReachableAndReturnError:nil]) + { + outProxy = appProxy; + } + } + } + }; + + if(allowedTypes & PERSISTENCE_HELPER_TYPE_USER) + { + [[LSApplicationWorkspace defaultWorkspace] enumerateApplicationsOfType:0 block:searchBlock]; + } + if(allowedTypes & PERSISTENCE_HELPER_TYPE_SYSTEM) + { + [[LSApplicationWorkspace defaultWorkspace] enumerateApplicationsOfType:1 block:searchBlock]; + } + + return outProxy; +} + +SecStaticCodeRef getStaticCodeRef(NSString *binaryPath) +{ + if(binaryPath == nil) + { + return NULL; + } + + CFURLRef binaryURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (__bridge CFStringRef)binaryPath, kCFURLPOSIXPathStyle, false); + if(binaryURL == NULL) + { + NSLog(@"[getStaticCodeRef] failed to get URL to binary %@", binaryPath); + return NULL; + } + + SecStaticCodeRef codeRef = NULL; + OSStatus result; + + result = SecStaticCodeCreateWithPathAndAttributes(binaryURL, kSecCSDefaultFlags, NULL, &codeRef); + + CFRelease(binaryURL); + + if(result != errSecSuccess) + { + NSLog(@"[getStaticCodeRef] failed to create static code for binary %@", binaryPath); + return NULL; + } + + return codeRef; +} + +NSDictionary* dumpEntitlements(SecStaticCodeRef codeRef) +{ + if(codeRef == NULL) + { + NSLog(@"[dumpEntitlements] attempting to dump entitlements without a StaticCodeRef"); + return nil; + } + + CFDictionaryRef signingInfo = NULL; + OSStatus result; + + result = SecCodeCopySigningInformation(codeRef, kSecCSRequirementInformation, &signingInfo); + + if(result != errSecSuccess) + { + NSLog(@"[dumpEntitlements] failed to copy signing info from static code"); + return nil; + } + + NSDictionary *entitlementsNSDict = nil; + + CFDictionaryRef entitlements = CFDictionaryGetValue(signingInfo, kSecCodeInfoEntitlementsDict); + if(entitlements == NULL) + { + NSLog(@"[dumpEntitlements] no entitlements specified"); + } + else if(CFGetTypeID(entitlements) != CFDictionaryGetTypeID()) + { + NSLog(@"[dumpEntitlements] invalid entitlements"); + } + else + { + entitlementsNSDict = (__bridge NSDictionary *)(entitlements); + NSLog(@"[dumpEntitlements] dumped %@", entitlementsNSDict); + } + + CFRelease(signingInfo); + return entitlementsNSDict; +} + +NSDictionary* dumpEntitlementsFromBinaryAtPath(NSString *binaryPath) +{ + // This function is intended for one-shot checks. Main-event functions should retain/release their own SecStaticCodeRefs + + if(binaryPath == nil) + { + return nil; + } + + SecStaticCodeRef codeRef = getStaticCodeRef(binaryPath); + if(codeRef == NULL) + { + return nil; + } + + NSDictionary *entitlements = dumpEntitlements(codeRef); + CFRelease(codeRef); + + return entitlements; +} + +NSDictionary* dumpEntitlementsFromBinaryData(NSData* binaryData) +{ + NSDictionary* entitlements; + NSString* tmpPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; + NSURL* tmpURL = [NSURL fileURLWithPath:tmpPath]; + if([binaryData writeToURL:tmpURL options:NSDataWritingAtomic error:nil]) + { + entitlements = dumpEntitlementsFromBinaryAtPath(tmpPath); + [[NSFileManager defaultManager] removeItemAtURL:tmpURL error:nil]; + } + return entitlements; +} \ No newline at end of file diff --git a/RootHelper/main.m b/RootHelper/main.m index 2bd0320..ed184cf 100644 --- a/RootHelper/main.m +++ b/RootHelper/main.m @@ -11,84 +11,6 @@ #import #import -typedef CF_OPTIONS(uint32_t, SecCSFlags) { - kSecCSDefaultFlags = 0 -}; -#define kSecCSRequirementInformation 1 << 2 -extern CFStringRef kSecCodeInfoEntitlementsDict; - -typedef struct __SecCode const *SecStaticCodeRef; -OSStatus SecStaticCodeCreateWithPathAndAttributes(CFURLRef path, SecCSFlags flags, CFDictionaryRef attributes, SecStaticCodeRef *staticCode); -OSStatus SecCodeCopySigningInformation(SecStaticCodeRef code, SecCSFlags flags, CFDictionaryRef *information); - -NSDictionary* dumpEntitlements(SecStaticCodeRef codeRef) -{ - if(codeRef == NULL) - { - NSLog(@"[dumpEntitlements] attempting to dump entitlements without a StaticCodeRef"); - return nil; - } - - CFDictionaryRef signingInfo = NULL; - OSStatus result; - - result = SecCodeCopySigningInformation(codeRef, kSecCSRequirementInformation, &signingInfo); - - if(result != errSecSuccess) - { - NSLog(@"[dumpEntitlements] failed to copy signing info from static code"); - return nil; - } - - NSDictionary *entitlementsNSDict = nil; - - CFDictionaryRef entitlements = CFDictionaryGetValue(signingInfo, kSecCodeInfoEntitlementsDict); - if(entitlements == NULL) - { - NSLog(@"[dumpEntitlements] no entitlements specified"); - } - else if(CFGetTypeID(entitlements) != CFDictionaryGetTypeID()) - { - NSLog(@"[dumpEntitlements] invalid entitlements"); - } - else - { - entitlementsNSDict = (__bridge NSDictionary *)(entitlements); - NSLog(@"[dumpEntitlements] dumped %@", entitlementsNSDict); - } - - CFRelease(signingInfo); - return entitlementsNSDict; -} -SecStaticCodeRef getStaticCodeRef(NSString *binaryPath) -{ - if(binaryPath == nil) - { - return NULL; - } - - CFURLRef binaryURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (__bridge CFStringRef)binaryPath, kCFURLPOSIXPathStyle, false); - if(binaryURL == NULL) - { - NSLog(@"[getStaticCodeRef] failed to get URL to binary %@", binaryPath); - return NULL; - } - - SecStaticCodeRef codeRef = NULL; - OSStatus result; - - result = SecStaticCodeCreateWithPathAndAttributes(binaryURL, kSecCSDefaultFlags, NULL, &codeRef); - - CFRelease(binaryURL); - - if(result != errSecSuccess) - { - NSLog(@"[getStaticCodeRef] failed to create static code for binary %@", binaryPath); - return NULL; - } - - return codeRef; -} NSSet* immutableAppBundleIdentifiers(void) { NSMutableSet* systemAppIdentifiers = [NSMutableSet new]; @@ -108,91 +30,68 @@ SecStaticCodeRef getStaticCodeRef(NSString *binaryPath) return systemAppIdentifiers.copy; } -NSDictionary* dumpEntitlementsFromBinaryAtPath(NSString *binaryPath) -{ - // This function is intended for one-shot checks. Main-event functions should retain/release their own SecStaticCodeRefs - - if(binaryPath == nil) - { - return nil; - } - - SecStaticCodeRef codeRef = getStaticCodeRef(binaryPath); - if(codeRef == NULL) - { - return nil; - } - - NSDictionary *entitlements = dumpEntitlements(codeRef); - CFRelease(codeRef); - - return entitlements; -} void refreshAppRegistrations() { - //registerPath((char*)trollStoreAppPath().UTF8String, 1); - registerPath((char*)trollStoreAppPath().UTF8String, 0); + registerPath((char*)trollStoreAppPath().UTF8String, 0, YES); for(NSString* appPath in trollStoreInstalledAppBundlePaths()) { - //registerPath((char*)appPath.UTF8String, 1); - registerPath((char*)appPath.UTF8String, 0); + registerPath((char*)appPath.UTF8String, 0, YES); } } -BOOL _installPersistenceHelper(LSApplicationProxy* appProxy, NSString* sourcePersistenceHelper, NSString* sourceRootHelper) +// Apparently there is some odd behaviour where TrollStore installed apps sometimes get restricted +// This works around that issue at least and is triggered when rebuilding icon cache +void cleanRestrictions(void) { - NSLog(@"_installPersistenceHelper(%@, %@, %@)", appProxy, sourcePersistenceHelper, sourceRootHelper); - - NSString* executablePath = appProxy.canonicalExecutablePath; - NSString* bundlePath = appProxy.bundleURL.path; - if(!executablePath) - { - NSBundle* appBundle = [NSBundle bundleWithPath:bundlePath]; - executablePath = [bundlePath stringByAppendingPathComponent:[appBundle objectForInfoDictionaryKey:@"CFBundleExecutable"]]; - } + NSString* clientTruthPath = @"/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/ClientTruth.plist"; + NSURL* clientTruthURL = [NSURL fileURLWithPath:clientTruthPath]; + NSDictionary* clientTruthDictionary = [NSDictionary dictionaryWithContentsOfURL:clientTruthURL]; - NSString* markPath = [bundlePath stringByAppendingPathComponent:@".TrollStorePersistenceHelper"]; - NSString* rootHelperPath = [bundlePath stringByAppendingPathComponent:@"trollstorehelper"]; + if(!clientTruthDictionary) return; - // remove existing persistence helper binary if exists - if([[NSFileManager defaultManager] fileExistsAtPath:markPath] && [[NSFileManager defaultManager] fileExistsAtPath:executablePath]) - { - [[NSFileManager defaultManager] removeItemAtPath:executablePath error:nil]; - } + NSArray* valuesArr; - // remove existing root helper binary if exists - if([[NSFileManager defaultManager] fileExistsAtPath:rootHelperPath]) + NSDictionary* lsdAppRemoval = clientTruthDictionary[@"com.apple.lsd.appremoval"]; + if(lsdAppRemoval && [lsdAppRemoval isKindOfClass:NSDictionary.class]) { - [[NSFileManager defaultManager] removeItemAtPath:rootHelperPath error:nil]; + NSDictionary* clientRestrictions = lsdAppRemoval[@"clientRestrictions"]; + if(clientRestrictions && [clientRestrictions isKindOfClass:NSDictionary.class]) + { + NSDictionary* unionDict = clientRestrictions[@"union"]; + if(unionDict && [unionDict isKindOfClass:NSDictionary.class]) + { + NSDictionary* removedSystemAppBundleIDs = unionDict[@"removedSystemAppBundleIDs"]; + if(removedSystemAppBundleIDs && [removedSystemAppBundleIDs isKindOfClass:NSDictionary.class]) + { + valuesArr = removedSystemAppBundleIDs[@"values"]; + } + } + } } - // install new persistence helper binary - if(![[NSFileManager defaultManager] copyItemAtPath:sourcePersistenceHelper toPath:executablePath error:nil]) - { - return NO; - } + if(!valuesArr || !valuesArr.count) return; - chmod(executablePath.UTF8String, 0755); - chown(executablePath.UTF8String, 33, 33); + NSMutableArray* valuesArrM = valuesArr.mutableCopy; + __block BOOL changed = NO; - NSError* error; - if(![[NSFileManager defaultManager] copyItemAtPath:sourceRootHelper toPath:rootHelperPath error:&error]) + [valuesArrM enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSString* value, NSUInteger idx, BOOL *stop) { - NSLog(@"error copying root helper: %@", error); - } + if(![value hasPrefix:@"com.apple."]) + { + [valuesArrM removeObjectAtIndex:idx]; + changed = YES; + } + }]; - chmod(rootHelperPath.UTF8String, 0755); - chown(rootHelperPath.UTF8String, 0, 0); + if(!changed) return; - // mark system app as persistence helper - if(![[NSFileManager defaultManager] fileExistsAtPath:markPath]) - { - [[NSFileManager defaultManager] createFileAtPath:markPath contents:[NSData data] attributes:nil]; - } + NSMutableDictionary* clientTruthDictionaryM = (__bridge_transfer NSMutableDictionary*)CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (__bridge CFDictionaryRef)clientTruthDictionary, kCFPropertyListMutableContainersAndLeaves); + + clientTruthDictionaryM[@"com.apple.lsd.appremoval"][@"clientRestrictions"][@"union"][@"removedSystemAppBundleIDs"][@"values"] = valuesArrM; - return YES; + [clientTruthDictionaryM writeToURL:clientTruthURL error:nil]; } @@ -219,12 +118,12 @@ int main(int argc, char *argv[], char *envp[]) { [dict setObject:[NSNumber numberWithInt:511] forKey:NSFilePosixPermissions]; [[NSFileManager defaultManager] setAttributes:dict ofItemAtPath:source error:nil]; } else if ([action isEqual: @"rebuildiconcache"]) { - [[LSApplicationWorkspace defaultWorkspace] _LSPrivateRebuildApplicationDatabasesForSystemApps:YES internal:YES user:YES]; - refreshAppRegistrations(); // needed for trollstore apps still working after rebuilding, otherwise they won't launch - respring(); + cleanRestrictions(); + [[LSApplicationWorkspace defaultWorkspace] _LSPrivateRebuildApplicationDatabasesForSystemApps:YES internal:YES user:YES]; + refreshAppRegistrations(); + killall(@"backboardd"); } - // NSLog(@"%s", getuid() == 0 ? "root" : "user"); return 0; } } \ No newline at end of file diff --git a/RootHelper/uicache.h b/RootHelper/uicache.h new file mode 100644 index 0000000..128391e --- /dev/null +++ b/RootHelper/uicache.h @@ -0,0 +1 @@ +extern void registerPath(char *path, int unregister, BOOL system); \ No newline at end of file diff --git a/RootHelper/uicache.m b/RootHelper/uicache.m new file mode 100644 index 0000000..cc4e469 --- /dev/null +++ b/RootHelper/uicache.m @@ -0,0 +1,263 @@ +@import Foundation; +@import CoreServices; +#import "CoreServices.h" +#import +#import "dlfcn.h" + +// uicache on steroids + +extern NSSet* immutableAppBundleIdentifiers(void); +extern NSDictionary* dumpEntitlementsFromBinaryAtPath(NSString* binaryPath); + +NSDictionary* constructGroupsContainersForEntitlements(NSDictionary* entitlements, BOOL systemGroups) +{ + if(!entitlements) return nil; + + NSString* entitlementForGroups; + NSString* mcmClass; + if(systemGroups) + { + entitlementForGroups = @"com.apple.security.system-groups"; + mcmClass = @"MCMSystemDataContainer"; + } + else + { + entitlementForGroups = @"com.apple.security.application-groups"; + mcmClass = @"MCMSharedDataContainer"; + } + + NSArray* groupIDs = entitlements[entitlementForGroups]; + if(groupIDs && [groupIDs isKindOfClass:[NSArray class]]) + { + NSMutableDictionary* groupContainers = [NSMutableDictionary new]; + + for(NSString* groupID in groupIDs) + { + MCMContainer* container = [NSClassFromString(mcmClass) containerWithIdentifier:groupID createIfNecessary:YES existed:nil error:nil]; + if(container.url) + { + groupContainers[groupID] = container.url.path; + } + } + + return groupContainers.copy; + } + + return nil; +} + +BOOL constructContainerizationForEntitlements(NSDictionary* entitlements) +{ + NSNumber* noContainer = entitlements[@"com.apple.private.security.no-container"]; + if(noContainer && [noContainer isKindOfClass:[NSNumber class]]) + { + if(noContainer.boolValue) + { + return NO; + } + } + + NSNumber* containerRequired = entitlements[@"com.apple.private.security.container-required"]; + if(containerRequired && [containerRequired isKindOfClass:[NSNumber class]]) + { + if(!containerRequired.boolValue) + { + return NO; + } + } + + return YES; +} + +NSString* constructTeamIdentifierForEntitlements(NSDictionary* entitlements) +{ + NSString* teamIdentifier = entitlements[@"com.apple.developer.team-identifier"]; + if(teamIdentifier && [teamIdentifier isKindOfClass:[NSString class]]) + { + return teamIdentifier; + } + return nil; +} + +NSDictionary* constructEnvironmentVariablesForContainerPath(NSString* containerPath) +{ + NSString* tmpDir = [containerPath stringByAppendingPathComponent:@"tmp"]; + return @{ + @"CFFIXED_USER_HOME" : containerPath, + @"HOME" : containerPath, + @"TMPDIR" : tmpDir + }; +} + +void registerPath(char* cPath, int unregister, BOOL system) +{ + if(!cPath) return; + NSString* path = [NSString stringWithUTF8String:cPath]; + + LSApplicationWorkspace* workspace = [LSApplicationWorkspace defaultWorkspace]; + if(unregister && ![[NSFileManager defaultManager] fileExistsAtPath:path]) + { + LSApplicationProxy* app = [LSApplicationProxy applicationProxyForIdentifier:path]; + if(app.bundleURL) + { + path = [app bundleURL].path; + } + } + + path = [path stringByResolvingSymlinksInPath]; + + NSDictionary* appInfoPlist = [NSDictionary dictionaryWithContentsOfFile:[path stringByAppendingPathComponent:@"Info.plist"]]; + NSString* appBundleID = [appInfoPlist objectForKey:@"CFBundleIdentifier"]; + + if([immutableAppBundleIdentifiers() containsObject:appBundleID.lowercaseString]) return; + + if(appBundleID && !unregister) + { + MCMContainer* appContainer = [NSClassFromString(@"MCMAppDataContainer") containerWithIdentifier:appBundleID createIfNecessary:YES existed:nil error:nil]; + NSString* containerPath = [appContainer url].path; + + NSMutableDictionary* dictToRegister = [NSMutableDictionary dictionary]; + + // Add entitlements + + NSString* appExecutablePath = [path stringByAppendingPathComponent:appInfoPlist[@"CFBundleExecutable"]]; + NSDictionary* entitlements = dumpEntitlementsFromBinaryAtPath(appExecutablePath); + if(entitlements) + { + dictToRegister[@"Entitlements"] = entitlements; + } + + // Misc + + dictToRegister[@"ApplicationType"] = system ? @"System" : @"User"; + dictToRegister[@"CFBundleIdentifier"] = appBundleID; + dictToRegister[@"CodeInfoIdentifier"] = appBundleID; + dictToRegister[@"CompatibilityState"] = @0; + if(containerPath) + { + dictToRegister[@"Container"] = containerPath; + dictToRegister[@"EnvironmentVariables"] = constructEnvironmentVariablesForContainerPath(containerPath); + } + dictToRegister[@"IsDeletable"] = @0; + dictToRegister[@"Path"] = path; + dictToRegister[@"IsContainerized"] = @(constructContainerizationForEntitlements(entitlements)); + dictToRegister[@"SignerOrganization"] = @"Apple Inc."; + dictToRegister[@"SignatureVersion"] = @132352; + dictToRegister[@"SignerIdentity"] = @"Apple iPhone OS Application Signing"; + dictToRegister[@"IsAdHocSigned"] = @YES; + dictToRegister[@"LSInstallType"] = @1; + dictToRegister[@"HasMIDBasedSINF"] = @0; + dictToRegister[@"MissingSINF"] = @0; + dictToRegister[@"FamilyID"] = @0; + dictToRegister[@"IsOnDemandInstallCapable"] = @0; + + NSString* teamIdentifier = constructTeamIdentifierForEntitlements(entitlements); + if(teamIdentifier) dictToRegister[@"TeamIdentifier"] = teamIdentifier; + + // Add group containers + + NSDictionary* appGroupContainers = constructGroupsContainersForEntitlements(entitlements, NO); + NSDictionary* systemGroupContainers = constructGroupsContainersForEntitlements(entitlements, NO); + NSMutableDictionary* groupContainers = [NSMutableDictionary new]; + [groupContainers addEntriesFromDictionary:appGroupContainers]; + [groupContainers addEntriesFromDictionary:systemGroupContainers]; + if(groupContainers.count) + { + if(appGroupContainers.count) + { + dictToRegister[@"HasAppGroupContainers"] = @YES; + } + if(systemGroupContainers.count) + { + dictToRegister[@"HasSystemGroupContainers"] = @YES; + } + dictToRegister[@"GroupContainers"] = groupContainers.copy; + } + + // Add plugins + + NSString* pluginsPath = [path stringByAppendingPathComponent:@"PlugIns"]; + NSArray* plugins = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:pluginsPath error:nil]; + + NSMutableDictionary* bundlePlugins = [NSMutableDictionary dictionary]; + for (NSString* pluginName in plugins) + { + NSString* pluginPath = [pluginsPath stringByAppendingPathComponent:pluginName]; + + NSDictionary* pluginInfoPlist = [NSDictionary dictionaryWithContentsOfFile:[pluginPath stringByAppendingPathComponent:@"Info.plist"]]; + NSString* pluginBundleID = [pluginInfoPlist objectForKey:@"CFBundleIdentifier"]; + + if(!pluginBundleID) continue; + MCMContainer* pluginContainer = [NSClassFromString(@"MCMPluginKitPluginDataContainer") containerWithIdentifier:pluginBundleID createIfNecessary:YES existed:nil error:nil]; + NSString* pluginContainerPath = [pluginContainer url].path; + + NSMutableDictionary* pluginDict = [NSMutableDictionary dictionary]; + + // Add entitlements + + NSString* pluginExecutablePath = [pluginPath stringByAppendingPathComponent:pluginInfoPlist[@"CFBundleExecutable"]]; + NSDictionary* pluginEntitlements = dumpEntitlementsFromBinaryAtPath(pluginExecutablePath); + if(pluginEntitlements) + { + pluginDict[@"Entitlements"] = pluginEntitlements; + } + + // Misc + + pluginDict[@"ApplicationType"] = @"PluginKitPlugin"; + pluginDict[@"CFBundleIdentifier"] = pluginBundleID; + pluginDict[@"CodeInfoIdentifier"] = pluginBundleID; + pluginDict[@"CompatibilityState"] = @0; + if(pluginContainerPath) + { + pluginDict[@"Container"] = pluginContainerPath; + pluginDict[@"EnvironmentVariables"] = constructEnvironmentVariablesForContainerPath(pluginContainerPath); + } + pluginDict[@"Path"] = pluginPath; + pluginDict[@"PluginOwnerBundleID"] = appBundleID; + pluginDict[@"IsContainerized"] = @(constructContainerizationForEntitlements(pluginEntitlements)); + pluginDict[@"SignerOrganization"] = @"Apple Inc."; + pluginDict[@"SignatureVersion"] = @132352; + pluginDict[@"SignerIdentity"] = @"Apple iPhone OS Application Signing"; + + NSString* pluginTeamIdentifier = constructTeamIdentifierForEntitlements(pluginEntitlements); + if(pluginTeamIdentifier) pluginDict[@"TeamIdentifier"] = pluginTeamIdentifier; + + // Add plugin group containers + + NSDictionary* pluginAppGroupContainers = constructGroupsContainersForEntitlements(pluginEntitlements, NO); + NSDictionary* pluginSystemGroupContainers = constructGroupsContainersForEntitlements(pluginEntitlements, NO); + NSMutableDictionary* pluginGroupContainers = [NSMutableDictionary new]; + [pluginGroupContainers addEntriesFromDictionary:pluginAppGroupContainers]; + [pluginGroupContainers addEntriesFromDictionary:pluginSystemGroupContainers]; + if(pluginGroupContainers.count) + { + if(pluginAppGroupContainers.count) + { + pluginDict[@"HasAppGroupContainers"] = @YES; + } + if(pluginSystemGroupContainers.count) + { + pluginDict[@"HasSystemGroupContainers"] = @YES; + } + pluginDict[@"GroupContainers"] = pluginGroupContainers.copy; + } + + [bundlePlugins setObject:pluginDict forKey:pluginBundleID]; + } + [dictToRegister setObject:bundlePlugins forKey:@"_LSBundlePlugins"]; + + if(![workspace registerApplicationDictionary:dictToRegister]) + { + NSLog(@"Error: Unable to register %@", path); + } + } + else + { + NSURL* url = [NSURL fileURLWithPath:path]; + if(![workspace unregisterApplication:url]) + { + NSLog(@"Error: Unable to unregister %@", path); + } + } +} diff --git a/TrollTools.xcodeproj/project.pbxproj b/TrollTools.xcodeproj/project.pbxproj index 2e7a4b5..111aa74 100644 --- a/TrollTools.xcodeproj/project.pbxproj +++ b/TrollTools.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ CE7D5CFD2906C2BD00EA26D2 /* FSOperations in Frameworks */ = {isa = PBXBuildFile; productRef = CE7D5CFC2906C2BD00EA26D2 /* FSOperations */; }; CE7D5D1D290706C600EA26D2 /* NSTask in Frameworks */ = {isa = PBXBuildFile; productRef = CE7D5D1C290706C600EA26D2 /* NSTask */; }; CE8DC8D52904C22500A1CBB0 /* RootHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8DC8D42904C22500A1CBB0 /* RootHelper.swift */; }; + CEECE830290C496D007E9496 /* IconOverridesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEECE82F290C496D007E9496 /* IconOverridesView.swift */; }; + CEECE832290C5405007E9496 /* AltIconSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEECE831290C5405007E9496 /* AltIconSelectionView.swift */; }; CEFE362E2904AC6400938D98 /* TSUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFE36292904AC6400938D98 /* TSUtil.m */; }; CEFE36632904AF8C00938D98 /* Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = CEFE36622904AF8C00938D98 /* Dynamic */; }; CEFE36642904B27B00938D98 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2BF83E2902E18900AD10BE /* CoreServices.framework */; }; @@ -65,6 +67,8 @@ CE76386C290AED160099C6F0 /* ProblemReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReporter.swift; sourceTree = ""; }; CE7D5D2229070FA600EA26D2 /* entitlements.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = entitlements.plist; sourceTree = ""; }; CE8DC8D42904C22500A1CBB0 /* RootHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootHelper.swift; sourceTree = ""; }; + CEECE82F290C496D007E9496 /* IconOverridesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconOverridesView.swift; sourceTree = ""; }; + CEECE831290C5405007E9496 /* AltIconSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltIconSelectionView.swift; sourceTree = ""; }; CEFE36262904A8B600938D98 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CEFE36292904AC6400938D98 /* TSUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSUtil.m; sourceTree = ""; }; CEFE362C2904AC6400938D98 /* TSUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSUtil.h; sourceTree = ""; }; @@ -97,6 +101,8 @@ CE2BF8152902E07F00AD10BE /* ThemesView.swift */, CE141B392903F20800AB48A7 /* ThemesSettingsView.swift */, CE141B3B2903F22F00AB48A7 /* ThemeView.swift */, + CEECE82F290C496D007E9496 /* IconOverridesView.swift */, + CEECE831290C5405007E9496 /* AltIconSelectionView.swift */, ); path = Themes; sourceTree = ""; @@ -294,6 +300,7 @@ files = ( CEFE362E2904AC6400938D98 /* TSUtil.m in Sources */, CE2BF8302902E07F00AD10BE /* PasscodeKeyFaceManager.swift in Sources */, + CEECE830290C496D007E9496 /* IconOverridesView.swift in Sources */, CE2BF82B2902E07F00AD10BE /* RootView.swift in Sources */, CE2BF8372902E10900AD10BE /* TrollToolsApp.swift in Sources */, CE2BF8262902E07F00AD10BE /* ImagePickerView.swift in Sources */, @@ -309,6 +316,7 @@ CE2BF8242902E07F00AD10BE /* Extensions.swift in Sources */, CE141B5A290460AE00AB48A7 /* WebclipsThemeManager.swift in Sources */, CE76386D290AED160099C6F0 /* ProblemReporter.swift in Sources */, + CEECE832290C5405007E9496 /* AltIconSelectionView.swift in Sources */, CE2BF82E2902E07F00AD10BE /* WallpaperGetter.swift in Sources */, CE141B3A2903F20800AB48A7 /* ThemesSettingsView.swift in Sources */, CE2BF82F2902E07F00AD10BE /* ThemeManager.swift in Sources */, diff --git a/TrollTools.xcodeproj/project.xcworkspace/xcuserdata/exerhythm.xcuserdatad/UserInterfaceState.xcuserstate b/TrollTools.xcodeproj/project.xcworkspace/xcuserdata/exerhythm.xcuserdatad/UserInterfaceState.xcuserstate index c1d5c0c..7e3961c 100644 Binary files a/TrollTools.xcodeproj/project.xcworkspace/xcuserdata/exerhythm.xcuserdatad/UserInterfaceState.xcuserstate and b/TrollTools.xcodeproj/project.xcworkspace/xcuserdata/exerhythm.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/TrollTools/Assets.xcassets/64.imageset/Contents.json b/TrollTools/Assets.xcassets/64.imageset/Contents.json new file mode 100644 index 0000000..7b9c6c0 --- /dev/null +++ b/TrollTools/Assets.xcassets/64.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "TrollUtils.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TrollTools/Assets.xcassets/64.imageset/TrollUtils.png b/TrollTools/Assets.xcassets/64.imageset/TrollUtils.png new file mode 100644 index 0000000..1519696 Binary files /dev/null and b/TrollTools/Assets.xcassets/64.imageset/TrollUtils.png differ diff --git a/TrollTools/Assets.xcassets/NotFound.imageset/Contents.json b/TrollTools/Assets.xcassets/NotFound.imageset/Contents.json new file mode 100644 index 0000000..f17e092 --- /dev/null +++ b/TrollTools/Assets.xcassets/NotFound.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "NotFound.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TrollTools/Assets.xcassets/NotFound.imageset/NotFound.png b/TrollTools/Assets.xcassets/NotFound.imageset/NotFound.png new file mode 100644 index 0000000..5fcd1a4 Binary files /dev/null and b/TrollTools/Assets.xcassets/NotFound.imageset/NotFound.png differ diff --git a/TrollTools/Other/Extensions.swift b/TrollTools/Other/Extensions.swift index 089d676..a584a36 100644 --- a/TrollTools/Other/Extensions.swift +++ b/TrollTools/Other/Extensions.swift @@ -7,6 +7,19 @@ import Foundation import SwiftUI +import Combine + +fileprivate var cancellables = [String : AnyCancellable] () + +public extension Published { + init(wrappedValue defaultValue: Value, key: String) { + let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue + self.init(initialValue: value) + cancellables[key] = projectedValue.sink { val in + UserDefaults.standard.set(val, forKey: key) + } + } +} extension StringProtocol { subscript(offset: Int) -> Character { self[index(startIndex, offsetBy: offset)] } diff --git a/TrollTools/Other/ProblemReporter.swift b/TrollTools/Other/ProblemReporter.swift index b87ae8b..dd20940 100644 --- a/TrollTools/Other/ProblemReporter.swift +++ b/TrollTools/Other/ProblemReporter.swift @@ -7,12 +7,12 @@ import Foundation -//func remLog(_ objs: Any...) { -// for obj in objs { -// let args: [CVarArg] = [ String(describing: obj) ] -// withVaList(args) { RLogv("%@", $0) } -// } -//} +func remLog(_ objs: Any...) { + for obj in objs { + let args: [CVarArg] = [ String(describing: obj) ] + withVaList(args) { RLogv("%@", $0) } + } +} //class ProblemReporter { // func report(_ str: String) { diff --git a/TrollTools/Other/RemoteLog.h b/TrollTools/Other/RemoteLog.h index d33bd1a..4be65bd 100644 --- a/TrollTools/Other/RemoteLog.h +++ b/TrollTools/Other/RemoteLog.h @@ -7,7 +7,7 @@ #import // change this to match your destination (server) IP address -#define RLOG_IP_ADDRESS "home.sourceloc.net" +#define RLOG_IP_ADDRESS "192.168.0.24" #define RLOG_PORT 11909 __attribute__((unused)) static void RLogv(NSString* format, va_list args) diff --git a/TrollTools/Other/TrollToolsApp.swift b/TrollTools/Other/TrollToolsApp.swift index 24d0228..bd9a38c 100644 --- a/TrollTools/Other/TrollToolsApp.swift +++ b/TrollTools/Other/TrollToolsApp.swift @@ -27,6 +27,7 @@ struct TrollToolsApp: App { } task.resume() } + try? RootHelper.loadMCM() } } } diff --git a/TrollTools/Private APIs/BadgeChanger.swift b/TrollTools/Private APIs/BadgeChanger.swift index 70ef7eb..bcde450 100644 --- a/TrollTools/Private APIs/BadgeChanger.swift +++ b/TrollTools/Private APIs/BadgeChanger.swift @@ -17,6 +17,20 @@ class BadgeChanger { badge.writeToCPBitmapFile(to: badgeBitmapPath as NSString) } + + static func change(to image: UIImage) throws { + let size = CGSize(width: 26, height: 26) + + UIGraphicsBeginImageContextWithOptions(size, false, 0.0) + image.draw(in: CGRect(origin: CGPoint.zero, size: size)) + let resizedImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + let badgeBitmapPath = "/var/mobile/Library/Caches/MappedImageCache/Persistent/SBIconBadgeView.BadgeBackground:26:26.cpbitmap" + try? FileManager.default.removeItem(atPath: badgeBitmapPath) + + resizedImage.writeToCPBitmapFile(to: badgeBitmapPath as NSString) + } } extension UIImage { @@ -39,6 +53,22 @@ extension UIImage { return img } + public func withRoundedCorners(radius: CGFloat? = nil) -> UIImage? { + let maxRadius = min(size.width, size.height) / 2 + let cornerRadius: CGFloat + if let radius = radius, radius > 0 && radius <= maxRadius { + cornerRadius = radius + } else { + cornerRadius = maxRadius + } + UIGraphicsBeginImageContextWithOptions(size, false, scale) + let rect = CGRect(origin: .zero, size: size) + UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).addClip() + draw(in: rect) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } } extension String: LocalizedError { diff --git a/TrollTools/Private APIs/PasscodeKeyFaceManager.swift b/TrollTools/Private APIs/PasscodeKeyFaceManager.swift index 637885b..8fc8e19 100644 --- a/TrollTools/Private APIs/PasscodeKeyFaceManager.swift +++ b/TrollTools/Private APIs/PasscodeKeyFaceManager.swift @@ -10,8 +10,8 @@ import UIKit class PasscodeKeyFaceManager { static let telephonyUIURL = URL(fileURLWithPath: "/var/mobile/Library/Caches/TelephonyUI-8") - static func setFace(_ image: UIImage, for n: Int) throws { - let size = CGSize(width: 150, height: 150) + static func setFace(_ image: UIImage, for n: Int, isBig: Bool) throws { + let size = isBig ? CGSize(width: 225, height: 225) : CGSize(width: 152, height: 152) UIGraphicsBeginImageContextWithOptions(size, false, 1.0) image.draw(in: CGRect(origin: .zero, size: size)) let newImage = UIGraphicsGetImageFromCurrentImageContext() @@ -26,7 +26,7 @@ class PasscodeKeyFaceManager { let fm = FileManager.default for imageURL in try fm.contentsOfDirectory(at: telephonyUIURL, includingPropertiesForKeys: nil) { - let size = CGSize(width: 150, height: 150) + let size = CGSize(width: 152, height: 152) UIGraphicsBeginImageContextWithOptions(size, false, 1.0) UIImage().draw(in: CGRect(origin: .zero, size: size)) let newImage = UIGraphicsGetImageFromCurrentImageContext() diff --git a/TrollTools/Private APIs/RootHelper.swift b/TrollTools/Private APIs/RootHelper.swift index 7dc196c..08b55ef 100644 --- a/TrollTools/Private APIs/RootHelper.swift +++ b/TrollTools/Private APIs/RootHelper.swift @@ -9,7 +9,7 @@ import UIKit import NSTaskBridge class RootHelper { - static let rootHelperPath = Bundle.main.url(forAuxiliaryExecutable: "trolltoolsroothelper")!.path + static let rootHelperPath = Bundle.main.url(forAuxiliaryExecutable: "trolltoolsroothelper")?.path ?? "/" static func move(from sourceURL: URL, to destURL: URL) throws { let code = spawnRoot(rootHelperPath, ["filemove", sourceURL.path, destURL.path], nil, nil) @@ -35,4 +35,8 @@ class RootHelper { let code = spawnRoot(rootHelperPath, ["rebuildiconcache", "", ""], nil, nil) guard code == 0 else { throw "Helper.rebuildIconCache: returned non-zero code \(code)" } } + static func loadMCM() throws { + let code = spawnRoot(rootHelperPath, ["", "", ""], nil, nil) + guard code == 0 else { throw "Helper.rebuildIconCache: returned non-zero code \(code)" } + } } diff --git a/TrollTools/Private APIs/Themes/CatalogThemeManager.swift b/TrollTools/Private APIs/Themes/CatalogThemeManager.swift index bac4018..7ad4487 100644 --- a/TrollTools/Private APIs/Themes/CatalogThemeManager.swift +++ b/TrollTools/Private APIs/Themes/CatalogThemeManager.swift @@ -11,71 +11,56 @@ import AssetCatalogWrapper class CatalogThemeManager { - static var fm = FileManager.default + var fm = FileManager.default - static func setTheme(theme: Theme, filenameEnding: String, apps: [LSApplicationProxy], progress: (String) -> ()) throws { - // Itterate over all icons - var systemApps: [LSApplicationProxy] = [] - - let appCount = apps.count - for (i,app) in apps.enumerated() { - guard let bundleID = app.bundleIdentifier else { throw "Bundle not found" } - func sendProgress(_ str: String) { - progress("App #\(i)/\(appCount)\n\(bundleID)\n\n\(str)") - } - - sendProgress("Starting") - guard let appURL = app.bundleURL else { continue } - - // check if it's in /var - sendProgress("Checking if app is in /var") - guard appURL.pathComponents.count >= 1 && (appURL.pathComponents[1] == "var" || appURL.pathComponents[1] == "private") else { - systemApps.append(app) - continue - } - - // Icon url - sendProgress("Getting url of icon in theme") - let themeIconURL = theme.url.appendingPathComponent("IconBundles").appendingPathComponent(bundleID + filenameEnding + ".png") - guard fm.fileExists(atPath: themeIconURL.path) else { continue } - - // Backup assets - let catalogURL = appURL.appendingPathComponent("Assets.car") - let backupURL = try backupAssetsURL(appURL: appURL) - - // Restore broken apps from backup - sendProgress("Checking if assets.car exists") - if !fm.fileExists(atPath: catalogURL.path) { - sendProgress("Catalog not found - \(catalogURL.path).\nChecking if backup exists") - if fm.fileExists(atPath: backupURL.path) { - sendProgress("Restoring from backup") - try RootHelper.copy(from: backupURL, to: catalogURL) - } else { continue } - } - - // Create backup if not made - sendProgress("Checking if backup exists") - if !fm.fileExists(atPath: backupURL.path) { - try RootHelper.copy(from: catalogURL, to: backupURL) - } - - // Get CGImage from icon - sendProgress("Creating icon CGImage from theme") - let imgData = try Data(contentsOf: themeIconURL) - guard let image = UIImage(data: imgData) else { continue } - guard let cgImage = image.cgImage else { continue } - - // Apply new icon - sendProgress("Start modifying") - try modifyIconInCatalog(url: catalogURL, to: cgImage, sendProgress: sendProgress(_:)) - sendProgress("Completed") + func applyChanges(_ changes: [ThemeManager.UserAppIconChange], progress: (Double) -> ()) throws { + let changesCount = Double(changes.count) + for (i,change) in changes.enumerated() { + try? applyChange(change) + progress(Double(i) / changesCount) + } + } + + private func applyChange(_ change: ThemeManager.UserAppIconChange) throws { + let appURL = change.bundleURL + let catalogURL = appURL.appendingPathComponent("Assets.car") + + if let iconURL = change.themeIconURL { + // MARK: Apply custom icon + + // Backup ass :troll: + let backupURL = try backupAssetsURL(appURL: appURL) + + // Restore broken apps from backup + if !fm.fileExists(atPath: catalogURL.path) { + if fm.fileExists(atPath: backupURL.path) { + try RootHelper.copy(from: backupURL, to: catalogURL) + } else { return } + } + + // Create backup if not made + if !fm.fileExists(atPath: backupURL.path) { + try RootHelper.copy(from: catalogURL, to: backupURL) + } + + // Get CGImage from icon + let imgData = try Data(contentsOf: iconURL) + guard let image = UIImage(data: imgData) else { return } + guard let cgImage = image.cgImage else { return } + + // Apply new icon + try modifyIconInCatalog(url: catalogURL, to: cgImage) + } else { + // MARK: Revert icon + guard fm.fileExists(atPath: catalogURL.path) else { return } + let backupURL = try backupAssetsURL(appURL: appURL) + guard fm.fileExists(atPath: backupURL.path) else { return } + try RootHelper.removeItem(at: catalogURL) + try RootHelper.move(from: backupURL, to: catalogURL) } - try WebclipsThemeManager.setTheme(theme: theme, filenameEnding: filenameEnding, apps: systemApps, progress: progress) - - progress("Completed.") } - static func restoreCatalogs(progress: (String) -> ()) throws { + func restoreCatalogs(progress: (String) -> ()) throws { guard let apps = LSApplicationWorkspace.default().allApplications() else { throw "Couldn't get apps" } let appCount = apps.count for (i, app) in apps.enumerated() { @@ -94,18 +79,15 @@ class CatalogThemeManager { } } - static func modifyIconInCatalog(url: URL, to icon: CGImage, sendProgress: (String) -> ()) throws { // icon: CGImage + func modifyIconInCatalog(url: URL, to icon: CGImage) throws { // icon: CGImage let tempAssetDir = URL(fileURLWithPath: "/var/mobile/.DO-NOT-DELETE-TrollTools/temp-assets-\(UUID()).car") - sendProgress("Moving assets to temp dir") try RootHelper.move(from: url, to: tempAssetDir) defer { try? RootHelper.move(from: tempAssetDir, to: url) } - sendProgress("Setting permission") try RootHelper.setPermission(url: tempAssetDir) - sendProgress("Getting renditions") let (catalog, renditionsRoot) = try AssetCatalogWrapper.shared.renditions(forCarArchive: tempAssetDir) for rendition in renditionsRoot { let type = rendition.type @@ -116,14 +98,12 @@ class CatalogThemeManager { try catalog.editItem(rend, fileURL: tempAssetDir, to: .image(icon)) } catch { // remLog("failed to edit rendition: \(error) \(rend.type) \(rend.name) \(rend.namedLookup)") - sendProgress("Editing icon asset failed \(rend.type) \(rend.name) \(rend.namedLookup)") } } } - sendProgress("Moving assets.car back into app's bundle") } - static private func backupAssetsURL(appURL: URL) throws -> URL { + private func backupAssetsURL(appURL: URL) throws -> URL { // Get version of app, so when app updates and user restores assets.car, old guard let infoPlistData = try? Data(contentsOf: appURL.appendingPathComponent("Info.plist")), let plist = try? PropertyListSerialization.propertyList(from: infoPlistData, format: nil) as? [String:Any] else { throw "Couldn't read template webclip plist" } guard let appShortVersion = (plist["CFBundleShortVersionString"] as? String) ?? plist["CFBundleVersion"] as? String else { throw "CFBundleShortVersionString missing for \(appURL.path)" } diff --git a/TrollTools/Private APIs/Themes/ThemeManager.swift b/TrollTools/Private APIs/Themes/ThemeManager.swift index 848dcee..c407c65 100644 --- a/TrollTools/Private APIs/Themes/ThemeManager.swift +++ b/TrollTools/Private APIs/Themes/ThemeManager.swift @@ -23,68 +23,155 @@ var webclipsActiveIconsDir: URL = { #endif }() -class ThemeManager { - static var fm = FileManager.default +class ThemeManager: ObservableObject { + let fm = FileManager.default + var catalogThemeManager = CatalogThemeManager() + var webclipsThemeManager = WebclipsThemeManager() - static func set(theme: Theme, progress: (String) -> ()) throws { - let iconBundlesURL = theme.url.appendingPathComponent("IconBundles") - try? fm.createDirectory(at: webclipsActiveIconsDir, withIntermediateDirectories: true) + @Published var preferedThemes: [Theme] = [] + + var preferedIcons: [String : ThemedIcon] { + get { + var res: [String : ThemedIcon] = [:] + for theme in preferedThemes { + if let icons = try? fm.contentsOfDirectory(at: theme.url, includingPropertiesForKeys: nil) { + for icon in icons { + let appID = appIDFromIcon(url: icon) + res[appID] = .init(appID: appID, themeName: theme.name) + } + } + } + for (overridenAppID, themeName) in iconOverrides { + res[overridenAppID] = .init(appID: overridenAppID, themeName: themeName) + } + return res + } + } + + var themes: [Theme] { + get { guard let data = UserDefaults.standard.data(forKey: "themes") else { return [] }; return (try? JSONDecoder().decode([Theme].self, from: data)) ?? [] } + set { guard let data = try? JSONEncoder().encode(newValue) else { return }; UserDefaults.standard.set(data, forKey: "themes") } + } + var iconOverrides: [String : String] { + get { return UserDefaults.standard.dictionary(forKey: "iconOverrides") as? [String : String] ?? [:] } + set { UserDefaults.standard.set(newValue, forKey: "iconOverrides") } + } + var currentThemedIcons: [ThemedIcon] { + get { guard let data = UserDefaults.standard.data(forKey: "currentThemedIcons") else { return [] }; return (try? JSONDecoder().decode([ThemedIcon].self, from: data)) ?? [] } + set { guard let data = try? JSONEncoder().encode(newValue) else { return }; UserDefaults.standard.set(data, forKey: "currentThemedIcons") } + } + + // MARK: - Set theme + func applyChanges(progress: (String) -> ()) throws { + let (userAppChanges, systemAppChanges) = neededChanges() - // Get theme icon ending like -large or @2x - let iconBundleContents = try fm.contentsOfDirectory(at: iconBundlesURL, includingPropertiesForKeys: nil) - guard let filenameEnding = (iconBundleContents - .first(where: { url1 in url1.lastPathComponent.contains("com.apple.AppStore") }) ?? iconBundleContents.last)? - .deletingPathExtension() - .lastPathComponent - .replacingOccurrences(of: "com.apple.AppStore", with: "") - else { throw "Invalid theme, no icons found" } - // Setting theme - guard let apps = LSApplicationWorkspace.default().allApplications() else { throw "Couldn't get apps" } - try CatalogThemeManager.setTheme(theme: theme, filenameEnding: filenameEnding, apps: apps, progress: progress) + try catalogThemeManager.applyChanges(userAppChanges, progress: { percentage in +// remLog(str) + progress("\(Int(percentage * 100))% done") + }) + try webclipsThemeManager.applyChanges(systemAppChanges, progress: { percentage in +// remLog(str) + progress("\(Int(percentage * 100))% done") + }) } - static func getIconFileEnding(iconFilename: String) throws -> String { - if iconFilename.contains("-large.png") { - return "-large" - } else if iconFilename.contains("@2x.png") { - return"@2x" - } else if iconFilename.contains("@23.png") { - return "@3x" - } else { - throw "Unknown icon filename ending. Couldn't get bundle ids. Please create an issue on github with the name of the theme you used. Thanks" + struct UserAppIconChange { + var bundleURL: URL + var themeIconURL: URL? + } + struct SystemAppIconChange { + var appID: String + var themeIconURL: URL? + var localizedName: String + } + func neededChanges() -> (user: [UserAppIconChange], system: [SystemAppIconChange]) { + let apps = LSApplicationWorkspace.default().allApplications() ?? [] + var userChanges: [UserAppIconChange] = [] + var systemChanges: [SystemAppIconChange] = [] + let preferedIcons = preferedIcons + + for app in apps { + let system = app.bundleURL.pathComponents[1] == "Applications" + if let themedIcon = preferedIcons[app.applicationIdentifier] { + // Icon needs to be themed + if system { + systemChanges.append(.init(appID: app.applicationIdentifier, themeIconURL: themedIcon.themeIconURL, localizedName: app.localizedName())) + } else { + userChanges.append(.init(bundleURL: app.bundleURL, themeIconURL: themedIcon.themeIconURL)) + } + + } else { + // Icon needs to be restored + if system { + systemChanges.append(.init(appID: app.applicationIdentifier, themeIconURL: nil, localizedName: app.localizedName())) + } else { + userChanges.append(.init(bundleURL: app.bundleURL, themeIconURL: nil)) + } + } } + return (userChanges, systemChanges) } - static func getIcons(forBundleIDs bundleIDs: [String], from theme: Theme) throws -> [UIImage?] { - bundleIDs.map { try? getIcon(forBundleID: $0, from: theme) } + // MARK: - Getting icons + func icons(forAppIDs appIDs: [String], from theme: Theme) throws -> [UIImage?] { + appIDs.map { try? icon(forAppID: $0, from: theme) } } - - static private func getIcon(forBundleID bundleID: String, from theme: Theme) throws -> UIImage { - let iconBundlesURL = theme.url.appendingPathComponent("IconBundles") - let filesInIconBundles = try fm.contentsOfDirectory(at: iconBundlesURL, includingPropertiesForKeys: nil) - guard let firstIconFilename = filesInIconBundles.last?.lastPathComponent else { throw "No icons" } - let iconFilenameEnding = try getIconFileEnding(iconFilename: firstIconFilename) - guard let image = UIImage(contentsOfFile: iconBundlesURL.appendingPathComponent(bundleID + iconFilenameEnding).path) else { throw "Couldn't open image" } + func icon(forAppID appID: String, from theme: Theme) throws -> UIImage { + remLog(theme.url.appendingPathComponent(appID).path + ".png") + guard let image = UIImage(contentsOfFile: theme.url.appendingPathComponent(appID).path + ".png") else { throw "Couldn't open image" } return image } + func icon(forAppID appID: String, fromThemeWithName name: String) throws -> UIImage { + return try icon(forAppID: appID, from: Theme(name: name, iconCount: 1)) + } - static func importTheme(from importURL: URL) throws -> Theme { - let name = importURL.deletingPathExtension().lastPathComponent + func importTheme(from importURL: URL) throws -> Theme { + var name = importURL.deletingPathExtension().lastPathComponent try? fm.createDirectory(at: themesDir, withIntermediateDirectories: true) - let themeURL = themesDir.appendingPathComponent(name) + var themeURL = themesDir.appendingPathComponent(name) + + if importURL.lastPathComponent.contains(".theme") { + for folder in (try? fm.contentsOfDirectory(at: importURL, includingPropertiesForKeys: nil)) ?? [] { + if folder.deletingPathExtension().lastPathComponent == "IconBundles" { + themeURL = folder + name = importURL.lastPathComponent.replacingOccurrences(of: ".theme", with: "") + } + } + } try? fm.removeItem(at: themeURL) try fm.createDirectory(at: themeURL, withIntermediateDirectories: true) - try fm.copyItem(at: importURL, to: themeURL.appendingPathComponent("IconBundles")) - return Theme(name: themeURL.deletingPathExtension().lastPathComponent, iconCount: try fm.contentsOfDirectory(at: themeURL.appendingPathComponent("IconBundles"), includingPropertiesForKeys: nil).count) + for icon in (try? fm.contentsOfDirectory(at: importURL, includingPropertiesForKeys: nil)) ?? [] { + try fm.copyItem(at: icon, to: themeURL.appendingPathComponent(appIDFromIcon(url: icon) + ".png")) + } + return Theme(name: themeURL.deletingPathExtension().lastPathComponent, iconCount: try fm.contentsOfDirectory(at: themeURL, includingPropertiesForKeys: nil).count) } - - static func removeImportedTheme(theme: Theme) throws { + func removeImportedTheme(theme: Theme) throws { try fm.removeItem(at: theme.url) } - static func getInstalledApplicationsNames() throws -> [String: String] { + func removeCurrentThemes(removeWebClips: Bool, progress: (String) -> ()) throws { + try catalogThemeManager.restoreCatalogs(progress: progress) + try webclipsThemeManager.removeCurrentThemes() + if removeWebClips { + try webclipsThemeManager.removeWebclips() + } + } + + // MARK: - Utils + func iconFileEnding(iconFilename: String) -> String { + if iconFilename.contains("-large.png") { + return "-large" + } else if iconFilename.contains("@2x.png") { + return"@2x" + } else if iconFilename.contains("@23.png") { + return "@3x" + } else { + return "" + } + } + func installedApplicationsNames() throws -> [String: String] { guard let apps = LSApplicationWorkspace.default().allApplications() else { throw "Couldn't get apps" } return apps.reduce(into: [String: String]()) { let applicationIdentifier = $1.applicationIdentifier @@ -92,14 +179,16 @@ class ThemeManager { $0[applicationIdentifier ?? ""] = displayName } } - static func removeCurrentThemes(progress: (String) -> ()) throws { - try CatalogThemeManager.restoreCatalogs(progress: progress) - try WebclipsThemeManager.removeCurrentThemes() + func appIDFromIcon(url: URL) -> String { + return url.deletingPathExtension().lastPathComponent.replacingOccurrences(of: iconFileEnding(iconFilename: url.lastPathComponent), with: "") + } + func iconURL(appID: String, in theme: Theme) -> URL { + return theme.url.appendingPathComponent(appID + ".png") } } -struct Theme: Codable, Identifiable { +struct Theme: Codable, Identifiable, Equatable { var id = UUID() var name: String @@ -107,8 +196,20 @@ struct Theme: Codable, Identifiable { var url: URL { // Documents/ImportedThemes/Theme.theme return themesDir.appendingPathComponent(name /*+ ".theme"*/) } + + static func == (lhs: Theme, rhs: Theme) -> Bool { + return lhs.name == rhs.name + } } enum IconThemingMethod: String { case webclips, appIcons } + +struct ThemedIcon: Codable { + var appID: String + var themeName: String + var themeIconURL: URL { + themesDir.appendingPathComponent(themeName).appendingPathComponent(appID + ".png") + } +} diff --git a/TrollTools/Private APIs/Themes/WebclipsThemeManager.swift b/TrollTools/Private APIs/Themes/WebclipsThemeManager.swift index f9629a6..07b3c64 100644 --- a/TrollTools/Private APIs/Themes/WebclipsThemeManager.swift +++ b/TrollTools/Private APIs/Themes/WebclipsThemeManager.swift @@ -8,43 +8,55 @@ import UIKit class WebclipsThemeManager { - static var templatePlistURL = Bundle.main.url(forResource: "WebClipTemplate", withExtension: "plist") - static var fm = FileManager.default + var templatePlistURL = Bundle.main.url(forResource: "WebClipTemplate", withExtension: "plist") + var fm = FileManager.default - static func setTheme(theme: Theme, filenameEnding: String, apps: [LSApplicationProxy], progress: (String) -> ()) throws { - // Itterate over all icons - let appCount = apps.count - for (i,app) in apps.enumerated() { - guard let bundleID = app.bundleIdentifier else { throw "Bundle not found" } - func sendProgress(_ str: String) { - progress("System App #\(i)/\(appCount)\n\(bundleID)\n\n\(str)") - } - sendProgress("Getting icon") - let themeIconURL = theme.url.appendingPathComponent("IconBundles").appendingPathComponent(bundleID + filenameEnding + ".png") - guard fm.fileExists(atPath: themeIconURL.path) else { continue } + func applyChanges(_ changes: [ThemeManager.SystemAppIconChange], progress: (Double) -> ()) throws { + let changesCount = Double(changes.count) + guard changesCount > 0 else { throw "No changes" } + for (i,change) in changes.enumerated() { + try? applyChange(change) + progress(Double(i) / changesCount) + } + } + + private func applyChange(_ change: ThemeManager.SystemAppIconChange) throws { + try? fm.createDirectory(at: webclipsActiveIconsDir, withIntermediateDirectories: true) + + let appID = change.appID + + if let iconURL = change.themeIconURL { + guard fm.fileExists(atPath: iconURL.path) else { return } + + let webClipURL = webClipURL(appID: appID) - let webClipURL = webClipURL(bundleID: bundleID) - sendProgress("adding webclip \(webClipURL.path)") // Add webclip if not added if !fm.fileExists(atPath: webClipURL.path) { - guard let displayName = app.localizedName() else { continue } - try addWebClip(bundleID: bundleID, displayName: displayName) + try addWebClip(bundleID: appID, displayName: change.localizedName) } // Copy icon to activeIconsDir - sendProgress("Setting icon") - let activeIconDir = webclipsActiveIconsDir.appendingPathComponent(bundleID + ".png") + let activeIconDir = webclipsActiveIconsDir.appendingPathComponent(appID + ".png") try? fm.removeItem(at: activeIconDir) - sendProgress("changine active icon symlink") - try fm.createSymbolicLink(at: activeIconDir, withDestinationURL: themeIconURL) + try fm.createSymbolicLink(at: activeIconDir, withDestinationURL: iconURL) + } else { + try? fm.removeItem(at: webclipsActiveIconsDir.appendingPathComponent(appID + ".png")) } } - static func removeCurrentThemes() throws { +// func setTheme(theme: Theme, apps: [LSApplicationProxy], progress: (String) -> ()) throws { +// // Itterate over all icons +// let appCount = apps.count +// for (i,app) in apps.enumerated() { +// +// } +// } + + func removeCurrentThemes() throws { try fm.removeItem(at: webclipsActiveIconsDir) } - static func removeWebclips() throws { + func removeWebclips() throws { for url in try fm.contentsOfDirectory(at: URL(fileURLWithPath: "/var/mobile/Library/WebClips/"), includingPropertiesForKeys: nil) { guard url.lastPathComponent.contains(".DO-NOT-DELETE-TrollTools-") else { continue } try fm.removeItem(at: url) @@ -52,16 +64,16 @@ class WebclipsThemeManager { } - static private func webClipURL(bundleID: String) -> URL { + private func webClipURL(appID: String) -> URL { #if targetEnvironment(simulator) - FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("WebClips/.DO-NOT-DELETE-TrollTools-\(bundleID).webclip") + FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("WebClips/.DO-NOT-DELETE-TrollTools-\(appID).webclip") #else - URL(fileURLWithPath: "/var/mobile/Library/WebClips/.DO-NOT-DELETE-TrollTools-\(bundleID).webclip") + URL(fileURLWithPath: "/var/mobile/Library/WebClips/.DO-NOT-DELETE-TrollTools-\(appID).webclip") #endif // return } - static func addWebClip(bundleID: String, displayName: String) throws { - let webClipURL = webClipURL(bundleID: bundleID) + func addWebClip(bundleID: String, displayName: String) throws { + let webClipURL = webClipURL(appID: bundleID) try fm.createDirectory(at: webClipURL, withIntermediateDirectories: true) // Load plist @@ -80,24 +92,24 @@ class WebclipsThemeManager { try plistData.write(to: webClipURL.appendingPathComponent("Info.plist")) } - static func changeLabelVisibility(visible: Bool) throws { - let installedAppNames = try ThemeManager.getInstalledApplicationsNames() - - for url in try fm.contentsOfDirectory(at: URL(fileURLWithPath: "/var/mobile/Library/WebClips/"), includingPropertiesForKeys: nil) { - guard url.lastPathComponent.contains(".DO-NOT-DELETE-TrollTools-") else { continue } - - let plistURL = url.appendingPathComponent("Info.plist") - - let data = try Data(contentsOf: plistURL) - guard var plist = try PropertyListSerialization.propertyList(from: data, format: nil) as? [String:Any] else { throw "Couldn't read template webclip plist" } - - // Modify values - guard let bundleID = plist["ApplicationBundleIdentifier"] as? String else { throw "Couldn't get bundle id of exisitng webclip. Webclip url \(url)" } - plist["Title"] = visible ? installedAppNames[bundleID] : " " - - // Save plist - let plistData = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) - try plistData.write(to: plistURL) - } - } +// func changeLabelVisibility(visible: Bool) throws { +// let installedAppNames = try ThemeManager.getInstalledApplicationsNames() +// +// for url in try fm.contentsOfDirectory(at: URL(fileURLWithPath: "/var/mobile/Library/WebClips/"), includingPropertiesForKeys: nil) { +// guard url.lastPathComponent.contains(".DO-NOT-DELETE-TrollTools-") else { continue } +// +// let plistURL = url.appendingPathComponent("Info.plist") +// +// let data = try Data(contentsOf: plistURL) +// guard var plist = try PropertyListSerialization.propertyList(from: data, format: nil) as? [String:Any] else { throw "Couldn't read template webclip plist" } +// +// // Modify values +// guard let bundleID = plist["ApplicationBundleIdentifier"] as? String else { throw "Couldn't get bundle id of exisitng webclip. Webclip url \(url)" } +// plist["Title"] = visible ? installedAppNames[bundleID] : " " +// +// // Save plist +// let plistData = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) +// try plistData.write(to: plistURL) +// } +// } } diff --git a/TrollTools/RootView.swift b/TrollTools/RootView.swift index f1617b5..7fa7fc1 100644 --- a/TrollTools/RootView.swift +++ b/TrollTools/RootView.swift @@ -8,6 +8,8 @@ import SwiftUI struct RootView: View { + @StateObject var themeManager = ThemeManager() + var body: some View { TabView { ThemesView() @@ -27,6 +29,7 @@ struct RootView: View { // Label("Apple card", systemImage: "creditcard") // } } + .environmentObject(themeManager) } } diff --git a/TrollTools/Views/BadgeChangerView.swift b/TrollTools/Views/BadgeChangerView.swift index 57d0240..c17c825 100644 --- a/TrollTools/Views/BadgeChangerView.swift +++ b/TrollTools/Views/BadgeChangerView.swift @@ -6,10 +6,13 @@ // import SwiftUI +import Photos struct BadgeChangerView: View { @State private var color = Color.red @State private var radius: CGFloat = 24 + @State private var showingImagePicker = false + @State private var image: UIImage? var body: some View { GeometryReader { proxy in @@ -28,41 +31,64 @@ struct BadgeChangerView: View { .frame(width: minSize / 2, height: minSize / 2) .cornerRadius(minSize / 8) ZStack { - Rectangle() - .fill(color) - .frame(width: minSize / 5, height: minSize / 5) - .cornerRadius(minSize * radius / 240) + if image == nil { + Rectangle() + .fill(color) + .frame(width: minSize / 5, height: minSize / 5) + .cornerRadius(minSize * radius / 240) + } else { + Image(uiImage: image!) + .resizable() + .frame(width: minSize / 5, height: minSize / 5) + } Text("1") .foregroundColor(.white) .font(.system(size: 45)) } - .offset(x: minSize / 12, y: -minSize / 22) + .offset(x: minSize / 12, y: -minSize / 12) } Text("TrollTools") .font(.title) .foregroundColor(.white) .fontWeight(.medium) HStack { - ColorPicker("Set the background color", selection: $color) + ColorPicker("Set badge color", selection: $color) .labelsHidden() .scaleEffect(1.5) .padding() Slider(value: $radius, in: 0...24) .frame(width: minSize / 2) } + Button(action: { + if image == nil { + showPicker() + } else { + image = nil + } + }) { + Text(image == nil ? "Custom image" : "Clear image") + .padding(10) + .background(Color.secondary) + .cornerRadius(8) + .foregroundColor(.init(uiColor14: .systemBackground)) + .padding(.top, 24) + } Button("Apply and respring", action: { do { - try BadgeChanger.change(to: UIColor(color), with: radius) + if image == nil { + try BadgeChanger.change(to: UIColor(color), with: radius) + } else { + try BadgeChanger.change(to: image!) + } respring() } catch { UIApplication.shared.alert(body:"An error occured. " + error.localizedDescription) } }) .padding(10) - .background(Color.blue) + .background(Color.accentColor) .cornerRadius(8) .foregroundColor(.white) - .padding(.top, 24) } } .navigationTitle("Badge Color") @@ -70,6 +96,17 @@ struct BadgeChangerView: View { } .navigationViewStyle(StackNavigationViewStyle()) } + .sheet(isPresented: $showingImagePicker) { + ImagePickerView(image: $image) + } + } + + func showPicker() { + PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in + DispatchQueue.main.async { + showingImagePicker = status == .authorized + } + } } } diff --git a/TrollTools/Views/PasscodeEditorView.swift b/TrollTools/Views/PasscodeEditorView.swift index ea14dd1..68c1e1d 100644 --- a/TrollTools/Views/PasscodeEditorView.swift +++ b/TrollTools/Views/PasscodeEditorView.swift @@ -14,6 +14,7 @@ struct PasscodeEditorView: View { @State private var faces: [UIImage?] = [UIImage?](repeating: nil, count: 10) @State private var changedFaces: [Bool] = [Bool](repeating: false, count: 10) @State private var changingFaceN = 0 + @State private var isBig = false var body: some View { GeometryReader { proxy in @@ -68,6 +69,10 @@ struct PasscodeEditorView: View { } } Spacer() + Button(isBig ? "Big" : "Small") { + isBig.toggle() + } + Spacer() Button("Remove all") { do { try PasscodeKeyFaceManager.removeAllFaces() @@ -99,7 +104,7 @@ struct PasscodeEditorView: View { .onChange(of: faces[changingFaceN] ?? UIImage()) { newValue in print(newValue) do { - try PasscodeKeyFaceManager.setFace(newValue, for: changingFaceN) + try PasscodeKeyFaceManager.setFace(newValue, for: changingFaceN, isBig: isBig) } catch { UIApplication.shared.alert(body: "An error occured while changing key face. \(error)") } diff --git a/TrollTools/Views/Themes/AltIconSelectionView.swift b/TrollTools/Views/Themes/AltIconSelectionView.swift new file mode 100644 index 0000000..4c4d950 --- /dev/null +++ b/TrollTools/Views/Themes/AltIconSelectionView.swift @@ -0,0 +1,56 @@ +// +// AltIconSelectionView.swift +// TrollTools +// +// Created by exerhythm on 28.10.2022. +// + +import SwiftUI + +struct AltIconSelectionView: View { + @EnvironmentObject var themeManager: ThemeManager + @State var bundleID: String + @State var displayName: String + @Environment(\.presentationMode) var presentation + var gridItemLayout = [GridItem(.adaptive(minimum: 100, maximum: 100))] + + var onChoose: (String) -> () + @State var icons: [(UIImage, String)] = [] + + var body: some View { + Group { + if icons.count == 0 { + Text("No themes containing icons for \(displayName) (\(bundleID)) have been found.") + .padding() + .background(Color(uiColor14: .secondarySystemBackground)) + .multilineTextAlignment(.center) + .cornerRadius(16) + .font(.footnote) + .foregroundColor(Color(uiColor14: .secondaryLabel)) + } else { + LazyVGrid(columns: gridItemLayout, spacing: 14) { + ForEach(icons, id: \.1) { (icon, themeName) in + Button(action: { + onChoose(themeName) + presentation.wrappedValue.dismiss() + }) { + Image(uiImage: icon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + .cornerRadius(25) + } + } + } + } + } + .navigationTitle(displayName) + .onAppear { + for t in themeManager.themes { + if let icon = try? themeManager.icon(forAppID: bundleID, from: t) { + icons.append((icon, t.name)) + } + } + } + } +} diff --git a/TrollTools/Views/Themes/IconOverridesView.swift b/TrollTools/Views/Themes/IconOverridesView.swift new file mode 100644 index 0000000..b4494dd --- /dev/null +++ b/TrollTools/Views/Themes/IconOverridesView.swift @@ -0,0 +1,126 @@ +// +// IndividualIconsEditorView.swift +// TrollTools +// +// Created by exerhythm on 28.10.2022. +// + +import SwiftUI +//import LaunchServicesBridge +import Dynamic + +struct IconOverridesView: View { + @EnvironmentObject var themeManager: ThemeManager + var gridItemLayout = [GridItem(.adaptive(minimum: 64, maximum: 64))] + + @State var allApps: [IconOverrideViewApp] = [] + + var body: some View { + ScrollView { + LazyVGrid(columns: gridItemLayout, spacing: 14) { + ForEach(allApps, id: \.self) { app in + IconEditorAppView(app: app, edited: themeManager.iconOverrides[app.appID] != nil, updateApps: updateApps) + .padding(.horizontal, 3) + .onAppear { + remLog(app.appID, themeManager.iconOverrides[app.appID]) + } + } + } + } + .navigationTitle("Icons override") + .onAppear { + updateApps() + } + } + + func updateApps() { + let preferedIcons = themeManager.preferedIcons + allApps = LSApplicationWorkspace.default().allApplications().compactMap { + if Dynamic($0).appTags.asArray?.contains("hidden") ?? true + || $0.isRestricted + || Dynamic($0).isLaunchProhibited.asBool ?? false + || (Bundle(url: $0.bundleURL)?.object(forInfoDictionaryKey: "SBAppTags") as? NSArray)?.contains("hidden") ?? false + { return nil } + if let themedIcon = preferedIcons[$0.applicationIdentifier] { + return IconOverrideViewApp(appID: $0.bundleIdentifier, + icon: UIImage(contentsOfFile: themedIcon.themeIconURL.path), displayName: $0.localizedName()) + } else { + return IconOverrideViewApp(appID: $0.bundleIdentifier, + icon: Dynamic(UIImage.self)._applicationIconImage(forBundleIdentifier: $0.bundleIdentifier, + format: 1, + scale: 4.0).asAnyObject as? UIImage, displayName: $0.localizedName()) + } + } + remLog("update") + } + + struct IconEditorAppView: View { + @EnvironmentObject var themeManager: ThemeManager + @State var app: IconOverrideViewApp + @State var edited: Bool + // @State var actionSheetPresented = false + @State var showsAltSelectionSheet = false + + var updateApps: () -> () + + var body: some View { + if !edited { + NavigationLink(destination: AltIconSelectionView(bundleID: app.appID, displayName: app.displayName, onChoose: { name in + themeManager.iconOverrides[app.appID] = name + edited = true + updateApps() + })) { + iconContent + } + } else { + Button(action: { + themeManager.iconOverrides[app.appID] = nil + edited = false + updateApps() + }) { + iconContent + } + } +// .actionSheet(isPresented: $actionSheetPresented) { +// ActionSheet(title: Text("Custom icon"), buttons: [ +// .cancel(), +// .default(Text("Alternative icons"), action: { +// }), +// .default(Text("Choose from photos")) +// ]) +// } + } + + @ViewBuilder + var iconContent: some View { + ZStack(alignment: .topTrailing) { + Image(uiImage: app.icon ?? UIImage(named: "NotFound")!) + .resizable() + .aspectRatio(contentMode: .fit) + .cornerRadius(12) + if edited { + Image(systemName: "lock.fill") + .foregroundColor(.init(uiColor14: .systemBackground)) + .padding(5) + .background(Color.accentColor) + .cornerRadius(.infinity) + .font(.system(size: 13)) + .offset(x: 7, y: -7) + } + } + } + } + + struct IconOverrideViewApp: Hashable { + var appID: String + var icon: UIImage? + var displayName: String + } + +} + +//struct IndividualIconsEditorView_Previews: PreviewProvider { +// static var previews: some View { +// IconOverridesView(editedApps: []) +// } +//} diff --git a/TrollTools/Views/Themes/IndividualIconsEditorView.swift b/TrollTools/Views/Themes/IndividualIconsEditorView.swift new file mode 100644 index 0000000..f1f7551 --- /dev/null +++ b/TrollTools/Views/Themes/IndividualIconsEditorView.swift @@ -0,0 +1,113 @@ +// +// IndividualIconsEditorView.swift +// TrollTools +// +// Created by exerhythm on 28.10.2022. +// + +import SwiftUI +//import LaunchServicesBridge +import Dynamic + +struct ThemeEditorView: View { + @EnvironmentObject var themeManager: ThemeManager + var gridItemLayout = [GridItem(.adaptive(minimum: 64, maximum: 64))] + var editedApps: [IconEditorApp] = { + (LSApplicationWorkspace.default().allApplications() ?? []).map { IconEditorApp(bundleID: $0.bundleIdentifier, icon: Dynamic(UIImage.self)._applicationIconImage(forBundleIdentifier: $0.bundleIdentifier, format: 1, scale: 4.0).asAnyObject as? UIImage ?? UIImage(named: "NotFound")!)} + + }() + + var body: some View { + ScrollView { + LazyVGrid(columns: gridItemLayout, spacing: 14) { + ForEach(editedApps, id: \.bundleID) { app in + IconEditorAppView(app: app) + .padding(.horizontal, 3) + } + } + } + .navigationTitle("Custom icons") + } + + struct IconEditorAppView: View { + @State var app: IconEditorApp + @State var edited = false + @State var actionSheetPresented = false + @State var showsAltSelectionSheet = false + + var body: some View { + Button(action: { + actionSheetPresented = true + }) { + ZStack(alignment: .topTrailing) { + Image(uiImage: app.icon ?? UIImage(named: "NotFound")!) + .resizable() + .aspectRatio(contentMode: .fit) + .cornerRadius(12) + if edited { + Image(systemName: "pencil") + .foregroundColor(.init(uiColor14: .systemBackground)) + .padding(5) + .background(Color.accentColor) + .cornerRadius(.infinity) + .font(.system(size: 13)) + .offset(x: 7, y: -7) + } + } + } + .actionSheet(isPresented: $actionSheetPresented) { + ActionSheet(title: Text("Custom icon"), buttons: [ + .cancel(), + .default(Text("Alternative icons"), action: { + showsAltSelectionSheet = true + }), + .default(Text("Choose from photos")) + ]) + } + .sheet(isPresented: $showsAltSelectionSheet) { + AltIconSelectionView(bundleID: app.bundleID, onChoose: { id in + + + }) + } + } + } +} + +struct IndividualIconsEditorView_Previews: PreviewProvider { + static var previews: some View { + ThemeEditorView(editedApps: [ + .init(bundleID: "com.apple.smth", icon: UIImage(named: "wallpaper")!), + .init(bundleID: "com.apple.smth1", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smth2", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smth3", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smth4", icon: UIImage(named: "wallpaper")!), + .init(bundleID: "com.apple.smth5", icon: UIImage(named: "wallpaper")!), + .init(bundleID: "com.apple.smth6", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smth7", icon: UIImage(named: "wallpaper")!), + .init(bundleID: "com.apple.smth8", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smth9", icon: UIImage(named: "wallpaper")!), + .init(bundleID: "com.apple.smtha", icon: UIImage(named: "wallpaper")!), + .init(bundleID: "com.apple.smthb", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smthc", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smthd", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smthe", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smthf", icon: UIImage(named: "wallpaper")!), + .init(bundleID: "com.apple.smthg", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smthh", icon: UIImage(named: "wallpaper")!), + .init(bundleID: "com.apple.smthi", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smthj", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smthk", icon: UIImage(named: "wallpaper")!), + .init(bundleID: "com.apple.smthl", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smthm", icon: UIImage(named: "wallpaper")!), + .init(bundleID: "com.apple.smthn", icon: UIImage(named: "64")!), + .init(bundleID: "com.apple.smtho", icon: UIImage(named: "wallpaper")!), + + ]) + } +} + +struct IconEditorApp { + var bundleID: String + var icon: UIImage? +} diff --git a/TrollTools/Views/Themes/ThemeView.swift b/TrollTools/Views/Themes/ThemeView.swift index 9bd4f8d..9d69c2c 100644 --- a/TrollTools/Views/Themes/ThemeView.swift +++ b/TrollTools/Views/Themes/ThemeView.swift @@ -9,11 +9,12 @@ import SwiftUI struct ThemeView: View { + @EnvironmentObject var themeManager: ThemeManager @State var theme: Theme - @State var isInUse: Bool var wallpaper: UIImage var defaultWallpaper: Bool = false - var applyTheme: (Theme) -> () + @State var icons: [UIImage?] = [] + @State var selected: Bool = false var body: some View { VStack { @@ -26,7 +27,7 @@ struct ThemeView: View { .clipped() .cornerRadius(8) .allowsHitTesting(false) - if let icons = try? ThemeManager.getIcons(forBundleIDs: ["com.apple.mobilephone", "com.apple.mobilesafari", "com.apple.mobileslideshow", "com.apple.camera", "com.apple.AppStore", "com.apple.Preferences", "com.apple.Music", "com.apple.calculator"], from: theme) { + if icons.count >= 8 { VStack { HStack { ForEach(icons[0...3], id: \.self) { @@ -65,29 +66,37 @@ struct ThemeView: View { Spacer() } Button(action: { - if !isInUse { - applyTheme(theme) + UIImpactFeedbackGenerator(style: .light).impactOccurred() + if selected { + themeManager.preferedThemes.removeAll { t in t.name == theme.name } } else { - UIApplication.shared.alert(title: "Use \"Clear current themes\"", body: "You can only turn off *all* themes.") + themeManager.preferedThemes.append(theme) } + selected.toggle() + remLog(themeManager.preferedIcons.keys.count, themeManager.preferedThemes.count) }) { - Text(isInUse ? "In use" : "Activate") + Text(selected ? "Selected" : "Select") .frame(maxWidth: .infinity) + } .padding(10) - .background(isInUse ? Color(red: 48 / 256, green: 209 / 256, blue: 88 / 256, opacity: 0.5) : Color(uiColor14: UIColor.tertiarySystemBackground)) + .background(selected ? Color.blue : Color(uiColor14: UIColor.tertiarySystemBackground)) .cornerRadius(8) - .foregroundColor(.init(uiColor14: .label)) + .foregroundColor(selected ? .white : .init(uiColor14: .label) ) } .padding(10) .background(Color(uiColor14: .secondarySystemBackground)) .cornerRadius(16) + .onAppear { + icons = (try? themeManager.icons(forAppIDs: ["com.apple.mobilephone", "com.apple.mobilesafari", "com.apple.mobileslideshow", "com.apple.camera", "com.apple.AppStore", "com.apple.Preferences", "com.apple.Music", "com.apple.calculator"], from: theme)) ?? [] + selected = themeManager.preferedThemes.contains(where: { t in t.name == theme.name }) + } } } struct ThemeView_Previews: PreviewProvider { static var previews: some View { - ThemeView(theme: Theme(name: "Theme", iconCount: 23), isInUse: true, wallpaper: UIImage(named: "wallpaper")!, applyTheme: { _ in}) + ThemeView(theme: Theme(name: "Theme", iconCount: 23), wallpaper: UIImage(named: "wallpaper")!) .frame(width: 190) } } diff --git a/TrollTools/Views/Themes/ThemesSettingsView.swift b/TrollTools/Views/Themes/ThemesSettingsView.swift index a417260..b3c9e7e 100644 --- a/TrollTools/Views/Themes/ThemesSettingsView.swift +++ b/TrollTools/Views/Themes/ThemesSettingsView.swift @@ -7,40 +7,40 @@ import SwiftUI -struct ThemesSettingsView: View { - @State var hidesLabels = UserDefaults.standard.bool(forKey: "hidesLabels") - - @Environment(\.horizontalSizeClass) var sizeClass - - - var body: some View { - List { - Toggle(isOn: $hidesLabels) { - Text("Hide WebClip Labels") - } - .onChange(of: hidesLabels, perform: { new in - UserDefaults.standard.set(new, forKey: "hidesLabels") - do { - try WebclipsThemeManager.changeLabelVisibility(visible: !hidesLabels) - respring() - } catch { - UIApplication.shared.alert(body: error.localizedDescription) - } - }) -// Button("Change theming method") { -// showsMethodChoosingPopover = true -// } -// .fullScreenCover(isPresented: $showsMethodChoosingPopover) { -// ThemesMethodChoosingView() +//struct ThemesSettingsView: View { +// @State var hidesLabels = UserDefaults.standard.bool(forKey: "hidesLabels") +// +// @Environment(\.horizontalSizeClass) var sizeClass +// +// +// var body: some View { +// List { +// Toggle(isOn: $hidesLabels) { +// Text("Hide WebClip Labels") // } - } - .listStyle(PlainListStyle()) - .frame(minWidth: 300, minHeight: 200) - } -} - -struct ThemesSettings_Previews: PreviewProvider { - static var previews: some View { - ThemesSettingsView() - } -} +// .onChange(of: hidesLabels, perform: { new in +// UserDefaults.standard.set(new, forKey: "hidesLabels") +// do { +// try WebclipsThemeManager.changeLabelVisibility(visible: !hidesLabels) +// respring() +// } catch { +// UIApplication.shared.alert(body: error.localizedDescription) +// } +// }) +//// Button("Change theming method") { +//// showsMethodChoosingPopover = true +//// } +//// .fullScreenCover(isPresented: $showsMethodChoosingPopover) { +//// ThemesMethodChoosingView() +//// } +// } +// .listStyle(PlainListStyle()) +// .frame(minWidth: 300, minHeight: 200) +// } +//} +// +//struct ThemesSettings_Previews: PreviewProvider { +// static var previews: some View { +// ThemesSettingsView() +// } +//} diff --git a/TrollTools/Views/Themes/ThemesView.swift b/TrollTools/Views/Themes/ThemesView.swift index 2139376..c9c7d3e 100644 --- a/TrollTools/Views/Themes/ThemesView.swift +++ b/TrollTools/Views/Themes/ThemesView.swift @@ -8,59 +8,117 @@ import SwiftUI struct ThemesView: View { + @EnvironmentObject var themeManager: ThemeManager @State private var isImporting = false + @State var isSelectingCustomIcons = false + @State var showsSettings = false + @State var wallpaper: UIImage? @State var defaultWallpaper = false private var gridItemLayout = [GridItem(.adaptive(minimum: 160))] + @State var themes: [Theme] = [] - @State var currentThemeIDs: [String] = [] - @State var showsSettings = false var body: some View { - NavigationView { - Group { - if themes.count == 0 { - Text("No themes imported. \nImport them using the button in the top right corner (Themes have to contain icons in the format of .png).") - .padding() - .background(Color(uiColor14: .secondarySystemBackground)) - .multilineTextAlignment(.center) - .cornerRadius(16) - .font(.footnote) - .foregroundColor(Color(uiColor14: .secondaryLabel)) - } else { - ScrollView { - LazyVGrid(columns: gridItemLayout, spacing: 8) { - ForEach(themes, id: \.url) { theme in - ThemeView(theme: theme, isInUse: currentThemeIDs.contains(theme.id.uuidString), wallpaper: wallpaper!, defaultWallpaper: defaultWallpaper, applyTheme: applyTheme) - .contextMenu { - Button { - themes.removeAll { theme1 in theme1.id == theme.id } - saveThemes() - do { - try ThemeManager.removeImportedTheme(theme: theme) - } catch { - UIApplication.shared.alert(body: error.localizedDescription) + ZStack { + NavigationView { + Group { + if themes.count == 0 { + Text("No themes imported. \nImport them using the button in the top right corner (Themes have to contain icons in the format of .png).") + .padding() + .background(Color(uiColor14: .secondarySystemBackground)) + .multilineTextAlignment(.center) + .cornerRadius(16) + .font(.footnote) + .foregroundColor(Color(uiColor14: .secondaryLabel)) + } else { + ScrollView { + LazyVGrid(columns: gridItemLayout, spacing: 8) { + ForEach(themes, id: \.name) { theme in + ThemeView(theme: theme, wallpaper: wallpaper!, defaultWallpaper: defaultWallpaper) + .contextMenu { + Button { + themes.removeAll { theme1 in theme1.id == theme.id } + themeManager.themes = themes + do { + try themeManager.removeImportedTheme(theme: theme) + } catch { + UIApplication.shared.alert(body: error.localizedDescription) + } + } label: { + Label("Remove theme", systemImage: "trash") } - } label: { - Label("Remove theme", systemImage: "trash") } + } + } + .padding(4) + + HStack { + VStack { + Text("TrollTools \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown")") + Text("Made by @sourcelocation.") + .font(.caption) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(10) + .background(Color(uiColor14: .secondarySystemBackground)) + .cornerRadius(16) + VStack { + HStack { + Text("Alternatives") + .font(.headline) + .lineLimit(1) + .minimumScaleFactor(0.7) + .padding(4) + + Text("ยท \(themeManager.iconOverrides.count)") + .font(.headline) + .foregroundColor(Color.secondary) + Spacer() } + NavigationLink(destination: IconOverridesView()) { + Text("Change") + .frame(maxWidth: .infinity) + .padding(10) + .background(Color(uiColor14: UIColor.tertiarySystemBackground)) + .cornerRadius(8) + .foregroundColor(.init(uiColor14: .label)) + } + } + .frame(maxWidth: .infinity) + .padding(10) + .background(Color(uiColor14: .secondarySystemBackground)) + .cornerRadius(16) } + .padding(.bottom, 80) } - .padding(4) - - Text("TrollTools \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown") by @sourcelocation.") - .font(.caption) - .foregroundColor(.secondary) - - .padding() + .padding(.horizontal, 6) } - .padding(.horizontal, 6) } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + isImporting = true + }) { + Image(systemName: "square.and.arrow.down") + } + } + ToolbarItem(placement: .navigationBarLeading) { + Button(action: { + UIApplication.shared.confirmAlert(title: "Full reset", body: "All app icons will be reverted to their original appearance and WebClips will be deleted. Are you sure you want to continue?", onOK: { + removeThemes(removeWebClips: true) + }, noCancel: false) + }) { + Image(systemName: "arrow.uturn.backward") + } + } + } + .navigationTitle("Themes") + .navigationBarTitleTextColor(Color(uiColor14: .label)) } - .navigationTitle("Themes") - .navigationBarTitleTextColor(Color(uiColor14: .label)) + .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear { wallpaper = WallpaperGetter.homescreen() if wallpaper == nil { @@ -68,27 +126,44 @@ struct ThemesView: View { defaultWallpaper = true } - if let data = UserDefaults.standard.data(forKey: "themes") { - themes = (try? JSONDecoder().decode([Theme].self, from: data)) ?? [] + // Old version check + var shouldReset = false + remLog(themesDir) + for themeURL in (try? FileManager.default.contentsOfDirectory(at: themesDir, includingPropertiesForKeys: nil)) ?? [] { + remLog(themeURL) + for icons in (try? FileManager.default.contentsOfDirectory(at: themeURL, includingPropertiesForKeys: nil)) ?? [] { + remLog(icons) + if icons.lastPathComponent == "IconBundles" { + shouldReset = true + break + } + } + } + if shouldReset { + UIApplication.shared.confirmAlert(title: "Theme reset required", body: "Due to major changes to the engine, a reset of themes is required.", onOK: { + try? FileManager.default.removeItem(at: themesDir) + UserDefaults.standard.set(nil, forKey: "themes") + UserDefaults.standard.set(nil, forKey: "currentThemeIDs") + removeThemes(removeWebClips: false) + }, noCancel: true) } - currentThemeIDs = UserDefaults.standard.array(forKey: "currentThemeIDs") as? [String] ?? [] + themes = themeManager.themes } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - isImporting = true - }) { - Image(systemName: "square.and.arrow.down") - } - } - ToolbarItem(placement: .navigationBarLeading) { - Button(action: { - UIApplication.shared.confirmAlert(title: "Remove custom icons", body: "All app icons will be reverted to their original appearance, but system app WebClips will remain. If you don't want them, you can safely delete them as any other normal app. Are you sure you want to continue?", onOK: { - removeThemes() - }, noCancel: false) - }) { - Image(systemName: "trash") + + VStack { + Spacer() + Button(action: { + applyChanges() + }) { + if themes.count > 0 { + Text("Apply changes") + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(8) + .padding() + .foregroundColor(.white) } } } @@ -96,7 +171,7 @@ struct ThemesView: View { .navigationViewStyle(StackNavigationViewStyle()) .fileImporter( isPresented: $isImporting, - allowedContentTypes: [.folder], + allowedContentTypes: [.folder, .init(filenameExtension: "theme")!], allowsMultipleSelection: false ) { result in guard let url = try? result.get().first else { UIApplication.shared.alert(body: "Couldn't get url of file. Did you select it?"); return } @@ -107,27 +182,25 @@ struct ThemesView: View { return } do { - let theme = try ThemeManager.importTheme(from: url) + let theme = try themeManager.importTheme(from: url) themes.append(theme) - saveThemes() + themeManager.themes = themes } catch { UIApplication.shared.alert(body: error.localizedDescription) } } } - - func applyTheme(_ theme: Theme) { + func applyChanges() { func apply() { let timeStart = Date() DispatchQueue.global(qos: .userInitiated).async { UIApplication.shared.alert(title: "Starting", body: "Please wait", animated: false, withButton: false) do { - try ThemeManager.set(theme: theme, progress: { str in + try themeManager.applyChanges(progress: { str in UIApplication.shared.change(title: "In progress", body: str) }) + UINotificationFeedbackGenerator().notificationOccurred(.success) UIApplication.shared.change(title: "Rebuilding Icon Cache...", body: "Device will respring after rebuild\n\nElapsed time: \(Double(Int(-timeStart.timeIntervalSinceNow * 100.0)) / 100.0)s") DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { try! RootHelper.rebuildIconCache() - currentThemeIDs.append(theme.id.uuidString) - UserDefaults.standard.set(currentThemeIDs, forKey: "currentThemeIDs") exit(0) // because it resprings }) } catch { UIApplication.shared.change(body: error.localizedDescription) } @@ -138,30 +211,28 @@ struct ThemesView: View { if FileManager.default.fileExists(atPath: app.bundleURL.appendingPathComponent("bak.car").path) { if !UserDefaults.standard.bool(forKey: "readAltAppsWarning") { found = true + UINotificationFeedbackGenerator().notificationOccurred(.warning) UIApplication.shared.confirmAlert(title: "Mugunghwa installed - PLEASE READ.", body: "It seems you've used other theming engines on this device. It is highly recommended resetting all their options to default values and removing the app.", onOK: { UserDefaults.standard.set(true, forKey: "readAltAppsWarning"); apply() }, noCancel: false) break } } } - if !found { apply() } - } - func saveThemes() { - guard let data = try? JSONEncoder().encode(themes) else { UIApplication.shared.alert(body: "Couldn't save themes"); return } - UserDefaults.standard.set(data, forKey: "themes") + if !found { + UIImpactFeedbackGenerator(style: .light).impactOccurred(); apply() + } } - func removeThemes() { + func removeThemes(removeWebClips: Bool) { DispatchQueue.global(qos: .userInitiated).async { UIApplication.shared.alert(title: "Starting", body: "Please wait", animated: false, withButton: false) - try? ThemeManager.removeCurrentThemes(progress: { str in + try? themeManager.removeCurrentThemes(removeWebClips: removeWebClips, progress: { str in UIApplication.shared.change(title: "In progress", body: str) }) DispatchQueue.main.async { - currentThemeIDs = [] - UserDefaults.standard.set([], forKey: "currentThemeIDs") + UINotificationFeedbackGenerator().notificationOccurred(.success) UIApplication.shared.change(title: "Rebuilding Icon Cache...", body: "Device will respring after rebuild") DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { try! RootHelper.rebuildIconCache() - exit(0) // because it resprings + // exit(0) // because it resprings }) } } diff --git a/build.command b/build.command index 15f704a..a1b41aa 100755 --- a/build.command +++ b/build.command @@ -9,6 +9,9 @@ APPLICATION_NAME=TrollTools CONFIGURATION=Debug cd build +if [ -e "$APPLICATION_NAME.tipa" ]; then +rm $APPLICATION_NAME.tipa +fi # Build .app xcodebuild -project "$WORKING_LOCATION/$APPLICATION_NAME.xcodeproj" \ @@ -61,4 +64,3 @@ cp -r $APPLICATION_NAME.app Payload/$APPLICATION_NAME.app zip -vr $APPLICATION_NAME.tipa Payload rm -rf $APPLICATION_NAME.app rm -rf Payload -zip -vr share.zip $APPLICATION_NAME.tipa diff --git a/entitlements.plist b/entitlements.plist index 108995a..35d3f3e 100644 --- a/entitlements.plist +++ b/entitlements.plist @@ -38,4 +38,4 @@ com.apple.private.WebClips.read-write - \ No newline at end of file +