From 168a89cb8acbcf4c53d2e08790d765859ef6680a Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 11 Nov 2026 15:16:54 +0800 Subject: [PATCH] Fix iOS 26 app manager blank state --- Tweak.m | 245 ++++++++++++++++++++++++++++++-- WQFilzaIntegration.h | 1 + WQFilzaIntegration.m | 116 ++++++++++++++- scripts/prepare_dev_terminal.sh | 14 +- 4 files changed, 361 insertions(+), 15 deletions(-) diff --git a/Tweak.m b/Tweak.m index 733aafe..2017272 100644 --- a/Tweak.m +++ b/Tweak.m @@ -407,6 +407,8 @@ static id hook_unZipFilePassword(id self, SEL _cmd, id zipPath, id toPath, id cu @interface LSApplicationWorkspace : NSObject + (id)defaultWorkspace; - (NSArray *)allApplications; +- (NSArray *)allInstalledApplications; +- (void)enumerateApplicationsOfType:(NSUInteger)type block:(void (^)(id proxy))block; @end // --- Helper: find app bundle path from bundleId --- @@ -490,6 +492,114 @@ static NSString *findIconPath(NSString *bundlePath, NSDictionary *infoPlist) { return bestIcon; } +// LaunchServices changed several proxy getters on iOS 26. Derive the bundle +// identifier from every available source instead of dropping the proxy. +static NSString *WQProxyString(id proxy, NSString *selectorName) +{ + SEL selector = NSSelectorFromString(selectorName); + if (!proxy || ![proxy respondsToSelector:selector]) return nil; + id value = ((id(*)(id, SEL))objc_msgSend)(proxy, selector); + return [value isKindOfClass:NSString.class] ? value : nil; +} + +static NSString *WQBundleIdentifierFromProxy(id proxy) +{ + if (!proxy) return nil; + NSArray *selectors = @[ + @"applicationIdentifier", @"bundleIdentifier", @"bundleId", + @"applicationID", + ]; + for (NSString *name in selectors) { + NSString *value = WQProxyString(proxy, name); + if (value.length) return value; + } + NSArray *keys = @[ + @"bundleIdentifier", @"applicationIdentifier", + @"_bundleIdentifier", @"_applicationIdentifier", + ]; + for (NSString *key in keys) { + @try { + id value = [proxy valueForKey:key]; + if ([value isKindOfClass:NSString.class] && [value length]) + return value; + } @catch (__unused NSException *exception) { + } + } + SEL urlSelector = NSSelectorFromString(@"bundleURL"); + if ([proxy respondsToSelector:urlSelector]) { + id url = ((id(*)(id, SEL))objc_msgSend)(proxy, urlSelector); + if ([url isKindOfClass:NSURL.class]) { + NSString *plistPath = [[(NSURL *)url path] + stringByAppendingPathComponent:@"Info.plist"]; + NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile: + plistPath]; + NSString *identifier = info[@"CFBundleIdentifier"]; + if ([identifier isKindOfClass:NSString.class] && identifier.length) + return identifier; + } + } + return nil; +} + +static NSArray *WQLSApplicationSources(id workspace, NSArray *original) +{ + static __thread BOOL collecting; + if (collecting) return original ?: @[]; + collecting = YES; + + NSMutableArray *result = [NSMutableArray array]; + NSMutableSet *seen = [NSMutableSet set]; + void (^append)(id) = ^(id proxy) { + if (!proxy) return; + NSValue *value = [NSValue valueWithNonretainedObject:proxy]; + if ([seen containsObject:value]) return; + [seen addObject:value]; + [result addObject:proxy]; + }; + for (id proxy in original ?: @[]) append(proxy); + + NSArray *arraySelectors = @[ + @"allInstalledApplications", @"unrestrictedApplications", + @"directionsApplications", @"placeholderApplications", + ]; + for (NSString *name in arraySelectors) { + SEL selector = NSSelectorFromString(name); + if (![workspace respondsToSelector:selector]) continue; + id value = ((id(*)(id, SEL))objc_msgSend)(workspace, selector); + if ([value isKindOfClass:NSArray.class]) + for (id proxy in value) append(proxy); + } + + NSArray *typedSelectors = @[ + @"applicationsOfType:", @"legacyApplicationProxiesListWithType:", + ]; + for (NSString *name in typedSelectors) { + SEL selector = NSSelectorFromString(name); + if (![workspace respondsToSelector:selector]) continue; + for (NSUInteger type = 0; type <= 1; type++) { + id value = ((id(*)(id, SEL, NSUInteger))objc_msgSend)(workspace, + selector, type); + if ([value isKindOfClass:NSArray.class]) + for (id proxy in value) append(proxy); + } + } + + SEL enumerateSelector = NSSelectorFromString( + @"enumerateApplicationsOfType:block:"); + if ([workspace respondsToSelector:enumerateSelector]) { + void (^enumerateBlock)(id) = ^(id proxy) { append(proxy); }; + ((void(*)(id, SEL, NSUInteger, id))objc_msgSend)(workspace, + enumerateSelector, 0, enumerateBlock); + } + + collecting = NO; + if (result.count != (original ?: @[]).count) + NSLog(@"[Tweak] LaunchServices sources: original=%lu merged=%lu", + (unsigned long)(original ?: @[]).count, + (unsigned long)result.count); + return result; +} + // --- Hook: allApplications fallback --- static IMP orig_allApplications = NULL; static id hook_allApplications(id self, SEL _cmd) { @@ -497,15 +607,20 @@ static id hook_allApplications(id self, SEL _cmd) { ? ((id(*)(id,SEL))orig_allApplications)(self, _cmd) : @[]; NSMutableArray *apps = [NSMutableArray array]; NSMutableSet *known = [NSMutableSet set]; - for (id proxy in origResult ?: @[]) { - NSString *identifier = [proxy respondsToSelector:@selector(applicationIdentifier)] - ? [proxy applicationIdentifier] : nil; - if (!identifier) identifier = [proxy respondsToSelector:@selector(bundleIdentifier)] - ? [proxy bundleIdentifier] : nil; - if (identifier.length) { - [known addObject:identifier]; - [apps addObject:proxy]; + NSMutableSet *knownBundlePaths = [NSMutableSet set]; + NSArray *sources = WQLSApplicationSources(self, origResult); + for (id proxy in sources) { + NSString *identifier = WQBundleIdentifierFromProxy(proxy); + if (identifier.length) [known addObject:identifier]; + SEL urlSelector = NSSelectorFromString(@"bundleURL"); + if ([proxy respondsToSelector:urlSelector]) { + id url = ((id(*)(id,SEL))objc_msgSend)(proxy, urlSelector); + if ([url isKindOfClass:NSURL.class] && [(NSURL *)url path].length) + [knownBundlePaths addObject:[(NSURL *)url path]]; } + // Keep proxies even when iOS 26 hides their identifier; the + // ApplicationItem hook derives it again from bundleURL/Info.plist. + [apps addObject:proxy]; } // Apps hidden from LaunchServices enumeration are recovered from their @@ -528,6 +643,48 @@ static id hook_allApplications(id self, SEL _cmd) { [known addObject:bundleID]; merged++; } + // Some iOS 26 builds expose only bundle identifiers here. + SEL identifiersSelector = NSSelectorFromString(@"installedApplications"); + if ([self respondsToSelector:identifiersSelector]) { + id identifiers = ((id(*)(id, SEL))objc_msgSend)(self, + identifiersSelector); + if ([identifiers isKindOfClass:NSArray.class]) { + for (id value in identifiers) { + if (![value isKindOfClass:NSString.class] || + [known containsObject:value]) continue; + NSString *bundleID = value; + NSString *bundlePath = WQBundlePathForIdentifier(bundleID); + if (bundlePath.length && + [knownBundlePaths containsObject:bundlePath]) continue; + id proxy = nil; + if ([proxyClass respondsToSelector: + NSSelectorFromString(@"applicationProxyForIdentifier:")]) { + proxy = ((id(*)(id,SEL,id))objc_msgSend)(proxyClass, + NSSelectorFromString(@"applicationProxyForIdentifier:"), + bundleID); + } + if (!proxy) { + NSString *error = nil; + NSString *dataPath = MCMFilzaDataContainerPath(bundleID, + &error); + NSMutableDictionary *info = [@{ + @"bundleID": bundleID, + @"name": WQFallbackDisplayName(bundleID), + @"version": @"1.0", + } mutableCopy]; + if (bundlePath.length) info[@"bundlePath"] = bundlePath; + if (dataPath.length) info[@"dataPath"] = dataPath; + proxy = [[WQApplicationProxy alloc] initWithAppInfo:info]; + } + if (proxy) { + [apps addObject:proxy]; + [known addObject:bundleID]; + merged++; + } + } + } + } + if (merged > 0 || apps.count > 0) { NSLog(@"[Tweak] Apps Manager: LaunchServices=%lu worse_query merged=%lu total=%lu", (unsigned long)(origResult.count), (unsigned long)merged, @@ -546,6 +703,17 @@ static id hook_allApplications(id self, SEL _cmd) { NSString *bid = info[@"CFBundleIdentifier"]; if (bid) { id proxy = [NSClassFromString(@"LSApplicationProxy") applicationProxyForIdentifier:bid]; + if (!proxy) { + proxy = [[WQApplicationProxy alloc] initWithAppInfo:@{ + @"bundleID": bid, + @"bundlePath": [uuidPath stringByAppendingPathComponent:item], + @"dataPath": WQDataPathForIdentifier(bid) ?: @"", + @"name": info[@"CFBundleDisplayName"] ?: + info[@"CFBundleName"] ?: bid, + @"version": info[@"CFBundleShortVersionString"] ?: + info[@"CFBundleVersion"] ?: @"1.0", + }]; + } if (proxy) [apps addObject:proxy]; } } @@ -560,6 +728,17 @@ static id hook_allApplications(id self, SEL _cmd) { NSString *bid = info[@"CFBundleIdentifier"]; if (bid) { id proxy = [NSClassFromString(@"LSApplicationProxy") applicationProxyForIdentifier:bid]; + if (!proxy) { + proxy = [[WQApplicationProxy alloc] initWithAppInfo:@{ + @"bundleID": bid, + @"bundlePath": [@"/Applications" stringByAppendingPathComponent:item], + @"dataPath": WQDataPathForIdentifier(bid) ?: @"", + @"name": info[@"CFBundleDisplayName"] ?: + info[@"CFBundleName"] ?: bid, + @"version": info[@"CFBundleShortVersionString"] ?: + info[@"CFBundleVersion"] ?: @"1.0", + }]; + } if (proxy) [apps addObject:proxy]; } } @@ -574,7 +753,13 @@ static void hook_setAppProxy(id self, SEL _cmd, id proxy) { ((void(*)(id,SEL,id))orig_setAppProxy)(self, _cmd, proxy); NSString *bundleId = [self performSelector:NSSelectorFromString(@"bundleId")]; - if (!bundleId) return; + if (!bundleId.length) { + bundleId = WQBundleIdentifierFromProxy(proxy); + if (bundleId.length) + ((void(*)(id,SEL,id))objc_msgSend)(self, + NSSelectorFromString(@"setBundleId:"), bundleId); + } + if (!bundleId.length) return; NSString *bundlePath = nil; NSString *currentFilePath = [self performSelector:NSSelectorFromString(@"filePath")]; @@ -675,6 +860,43 @@ static void hook_didSelectItem(id self, SEL _cmd, id browserView, id indexPath) ((void(*)(id,SEL,id,id))orig_didSelectItem)(self, _cmd, browserView, indexPath); } +static IMP orig_appsViewDidAppear; + +static void hook_appsViewDidAppear(id self, SEL _cmd, BOOL animated) +{ + if (orig_appsViewDidAppear) + ((void(*)(id, SEL, BOOL))orig_appsViewDidAppear)(self, _cmd, animated); + + static char kReloadKey; + if (objc_getAssociatedObject(self, &kReloadKey)) return; + objc_setAssociatedObject(self, &kReloadKey, @YES, + OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + __block NSInteger attempts = 0; + __block __weak void (^weakRetry)(void); + void (^retry)(void) = ^{ + NSArray *list = nil; + SEL listSelector = NSSelectorFromString(@"fileList"); + if ([self respondsToSelector:listSelector]) + list = ((id(*)(id, SEL))objc_msgSend)(self, listSelector); + if ([list isKindOfClass:NSArray.class] && list.count > 0) return; + if (attempts++ >= 12) return; + SEL loadSelector = NSSelectorFromString(@"loadFilesWithSortMode:"); + if ([self respondsToSelector:loadSelector]) + ((void(*)(id, SEL, int))objc_msgSend)(self, loadSelector, 0); + if (attempts == 1) + NSLog(@"[Tweak] Apps Manager was empty; retrying after discovery"); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), + dispatch_get_main_queue(), ^{ + void (^strongRetry)(void) = weakRetry; + if (strongRetry) strongRetry(); + }); + }; + weakRetry = retry; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), + dispatch_get_main_queue(), retry); +} + #pragma mark - License / Integrity Bypass // Suppress "Main binary was modified" and "Not activated" alerts. @@ -2091,6 +2313,11 @@ static void installHooks(void) { if (appsVC) { Method m = class_getInstanceMethod(appsVC, NSSelectorFromString(@"browserView:didSelectItemAtIndexPath:")); if (m) { orig_didSelectItem = method_getImplementation(m); method_setImplementation(m, (IMP)hook_didSelectItem); } + m = class_getInstanceMethod(appsVC, @selector(viewDidAppear:)); + if (m) { + orig_appsViewDidAppear = method_getImplementation(m); + method_setImplementation(m, (IMP)hook_appsViewDidAppear); + } } NSLog(@"[Tweak] All hooks installed"); diff --git a/WQFilzaIntegration.h b/WQFilzaIntegration.h index 5a5cd37..30cbe95 100644 --- a/WQFilzaIntegration.h +++ b/WQFilzaIntegration.h @@ -52,3 +52,4 @@ FOUNDATION_EXPORT void WQRefreshDiscovery(void); FOUNDATION_EXPORT NSArray * _Nonnull WQChildrenForRoot(NSString * _Nonnull root); FOUNDATION_EXPORT NSString *_Nullable WQIdentifyContainerPath(NSString * _Nonnull path); FOUNDATION_EXPORT NSArray * _Nonnull WQActiveGrantHandles(void); +FOUNDATION_EXPORT NSString * _Nonnull WQFallbackDisplayName(NSString * _Nonnull bundleID); diff --git a/WQFilzaIntegration.m b/WQFilzaIntegration.m index e58ea54..1ba780c 100644 --- a/WQFilzaIntegration.m +++ b/WQFilzaIntegration.m @@ -698,7 +698,7 @@ static void WQInstallAppLinks(NSArray *> *a (unsigned long)uuidLinked, (unsigned long)apps.count); } -static NSString *WQFallbackDisplayName(NSString *bundleID) +NSString *WQFallbackDisplayName(NSString *bundleID) { static NSDictionary *names; static dispatch_once_t onceToken; @@ -968,6 +968,101 @@ static void WQDiscoverPerContainerFamilies(void) } } +static NSDictionary *WQMobileInstallationApps(void) +{ + static NSDictionary *apps; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + void *handle = dlopen( + "/System/Library/PrivateFrameworks/MobileInstallation.framework/MobileInstallation", + RTLD_LAZY); + if (!handle) { + NSLog(@"[WQ] MobileInstallation framework unavailable: %s", dlerror()); + return; + } + NSDictionary *(*lookup)(NSDictionary *, NSError **) = + (NSDictionary *(*)(NSDictionary *, NSError **))dlsym(handle, + "MobileInstallationLookup"); + if (lookup) { + NSError *error = nil; + id value = lookup(nil, &error); + if ([value isKindOfClass:NSDictionary.class]) apps = [value copy]; + NSLog(@"[WQ] MobileInstallationLookup apps=%lu error=%@", + (unsigned long)apps.count, + error.localizedDescription ?: @"none"); + } else { + NSLog(@"[WQ] MobileInstallationLookup symbol missing"); + } + dlclose(handle); + }); + return apps ?: @{}; +} + +static void WQAppendMobileInstallationApps(NSMutableArray *result, + NSMutableSet *known) +{ + NSDictionary *apps = WQMobileInstallationApps(); + for (NSString *key in apps) { + NSDictionary *entry = apps[key]; + if (![entry isKindOfClass:NSDictionary.class]) continue; + NSString *bundleID = entry[@"CFBundleIdentifier"]; + if (![bundleID isKindOfClass:NSString.class] || !bundleID.length) + bundleID = key; + if (!bundleID.length || [known containsObject:bundleID]) continue; + + id containerValue = entry[@"Container"]; + NSString *dataPath = [containerValue isKindOfClass:NSURL.class] + ? [(NSURL *)containerValue path] : containerValue; + if (![dataPath isKindOfClass:NSString.class] || !dataPath.length) { + NSString *error = nil; + dataPath = MCMFilzaDataContainerPath(bundleID, &error); + } + id bundleValue = entry[@"Path"]; + NSString *bundlePath = [bundleValue isKindOfClass:NSURL.class] + ? [(NSURL *)bundleValue path] : bundleValue; + if (![bundlePath isKindOfClass:NSString.class] || !bundlePath.length) { + bundleValue = entry[@"BundlePath"]; + bundlePath = [bundleValue isKindOfClass:NSURL.class] + ? [(NSURL *)bundleValue path] : bundleValue; + } + if (![bundlePath isKindOfClass:NSString.class] || !bundlePath.length) + bundlePath = WQBundlePathForIdentifier(bundleID); + + NSMutableDictionary *info = [@{ + @"bundleID": bundleID, + @"name": bundleID, + @"version": @"1.0", + } mutableCopy]; + if (dataPath.length) info[@"dataPath"] = dataPath; + if (bundlePath.length) { + info[@"bundlePath"] = bundlePath; + NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile: + [bundlePath stringByAppendingPathComponent:@"Info.plist"]]; + NSString *name = plist[@"CFBundleDisplayName"]; + if (!name.length) name = plist[@"CFBundleName"]; + if (name.length) info[@"name"] = name; + NSString *version = plist[@"CFBundleShortVersionString"]; + if (!version.length) version = plist[@"CFBundleVersion"]; + if (version.length) info[@"version"] = version; + } + NSString *name = entry[@"CFBundleDisplayName"]; + if (!name.length) name = entry[@"CFBundleName"]; + if ([name isKindOfClass:NSString.class] && name.length) + info[@"name"] = name; + NSString *version = entry[@"CFBundleShortVersionString"]; + if (!version.length) version = entry[@"CFBundleVersion"]; + if ([version isKindOfClass:NSString.class] && version.length) + info[@"version"] = version; + if ([info[@"name"] isEqualToString:bundleID]) + info[@"name"] = WQFallbackDisplayName(bundleID); + [result addObject:info]; + [known addObject:bundleID]; + } + if (apps.count) + NSLog(@"[WQ] MobileInstallation merged=%lu total=%lu", + (unsigned long)apps.count, (unsigned long)result.count); +} + static NSArray *> *WQBuildAppList(void) { NSString *root = @"/private/var/mobile/Containers/Data/Application"; @@ -984,7 +1079,12 @@ static NSArray *> *WQBuildAppList(void) if (WQGrantContainer(container)) granted++; } WQPostStartupProgress(0.48, @"正在从数据目录识别应用…"); - NSArray *apps = WQAppInfoFromContainers(containers); + NSArray *containerApps = WQAppInfoFromContainers(containers); + NSMutableArray *apps = [containerApps mutableCopy]; + NSMutableSet *known = [NSMutableSet set]; + for (NSDictionary *info in containerApps) + if (info[@"bundleID"]) [known addObject:info[@"bundleID"]]; + WQAppendMobileInstallationApps(apps, known); WQInstallAppLinks(apps); NSLog(@"[WQ] app discovery containers=%lu granted=%lu apps=%lu", (unsigned long)containers.count, (unsigned long)granted, @@ -1000,7 +1100,9 @@ NSArray *> *WQInstalledApplications(void) dispatch_sync(gAppsQueue, ^{ NSTimeInterval age = gAppsCacheDate ? -[gAppsCacheDate timeIntervalSinceNow] : 1e9; - if (!gAppsCache || age > 120) { + // Never cache an empty result: the first call can happen before the + // worse_query grants and app-container scan have finished. + if (!gAppsCache || gAppsCache.count == 0 || age > 120) { gAppsCache = WQBuildAppList(); gAppsCacheDate = [NSDate date]; } @@ -1133,6 +1235,14 @@ NSString *WQBundlePathForIdentifier(NSString *bundleID) NSDictionary *info = gBundleInfo[bundleID]; if ([info[@"path"] isKindOfClass:NSString.class]) return info[@"path"]; + NSDictionary *installation = WQMobileInstallationApps()[bundleID]; + if ([installation isKindOfClass:NSDictionary.class]) { + NSString *path = installation[@"Path"]; + if (![path isKindOfClass:NSString.class] || !path.length) + path = installation[@"BundlePath"]; + if ([path isKindOfClass:NSString.class] && path.length) return path; + } + Class proxyClass = NSClassFromString(@"LSApplicationProxy"); id proxy = [proxyClass respondsToSelector: NSSelectorFromString(@"applicationProxyForIdentifier:")] diff --git a/scripts/prepare_dev_terminal.sh b/scripts/prepare_dev_terminal.sh index 73b3495..9945557 100755 --- a/scripts/prepare_dev_terminal.sh +++ b/scripts/prepare_dev_terminal.sh @@ -49,9 +49,9 @@ def exclude_framework(name): if n in ('libgdal', 'openblas', 'libgfortran', 'libproj', 'libgeos', 'libgeos_c', 'libspatialindex', 'libspatialindex_c'): return 'Python geo/math' - # Duplicate variant frameworks (A/B/C/D/E) are runtime alternatives. - if re.search(r'(dash|perl|ssh_cmd|openrsync|xetex|luahbtex|luatex|pdftex|euptex)[A-E]$', n): - return 'duplicate variant' + # Keep the A-E variants. ios_system picks a free variant for each + # concurrent script/shell/SSH session, so stripping them breaks dash + # scripts and parallel sessions. return None fw_size = {} @@ -133,6 +133,14 @@ while pending: selected.add(d) pending.append(d) +# ios_system appends A-E when it starts concurrent dash scripts. These +# variants are not referenced by commandDictionary.plist, so add them +# explicitly; otherwise `dash script.sh` fails to load dashA.framework. +for variant in ('dashA', 'dashB', 'dashC', 'dashD', 'dashE'): + framework = variant + '.framework' + if framework in fw_size: + selected.add(framework) + print('selected frameworks:', len(selected), 'size:', round(sum(fw_size[f] for f in selected)/1e6, 1), 'MB') # extract frameworks