Fix iOS 26 app manager blank state

This commit is contained in:
admin committed 2026-11-11 15:16:54 +08:00
1 parent 53498ed08f
commit 168a89cb8a
4 files changed
+361 -15

No files matched your search

+236 -9
View File
@@ -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<NSString *> *selectors = @[
@"applicationIdentifier", @"bundleIdentifier", @"bundleId",
@"applicationID",
];
for (NSString *name in selectors) {
NSString *value = WQProxyString(proxy, name);
if (value.length) return value;
}
NSArray<NSString *> *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<NSValue *> *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<NSString *> *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<NSString *> *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<NSString *> *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<NSString *> *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");