Fix iOS 26 app manager blank state
This commit is contained in:
1 parent
53498ed08f
commit
168a89cb8a
4 files changed
+361
-15
No files matched your search
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user