From dc51a3a3c79a7b516b631d98097405ea3e128824 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 12 Nov 2026 13:39:00 +0800 Subject: [PATCH] App Manager: per-entry traversal fallback and stable list --- README.md | 3 + Tweak.m | 13 +++ WQFilzaIntegration.h | 3 + WQFilzaIntegration.m | 239 ++++++++++++++++++++++++++++++++++++++----- 4 files changed, 234 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1ffc381..cd478b8 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,9 @@ filza_develop.ipa - App discovery uses the LaunchServices container list instead of a full inode scan on supported versions, so the first launch and App Manager open much faster. +- Entering App Manager also runs a fresh per-container worse_query/pathleak + traversal and falls back to per-app MCM/container lookups, so the list still + appears when the LaunchServices database cannot be read. - App icons are used where iOS returns them (`_applicationIconImageForBundle-` `Identifier:format:scale:`); otherwise Filza's placeholder is shown. - Accessible roots, App Data (by app), App Data (UUID), App Groups and System diff --git a/Tweak.m b/Tweak.m index 3e49203..2b088b2 100644 --- a/Tweak.m +++ b/Tweak.m @@ -941,6 +941,19 @@ static void hook_appsViewDidAppear(id self, SEL _cmd, BOOL animated) if (orig_appsViewDidAppear) ((void(*)(id, SEL, BOOL))orig_appsViewDidAppear)(self, _cmd, animated); + // App Manager entry always starts a fresh per-container traversal. Do not + // reuse the startup list: apps may have been installed since then and the + // LaunchServices database may be unavailable on this device. + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + WQRequestAppManagerRescan(); + WQInstalledApplications(); + dispatch_async(dispatch_get_main_queue(), ^{ + SEL loadSelector = NSSelectorFromString(@"loadFilesWithSortMode:"); + if ([self respondsToSelector:loadSelector]) + ((void(*)(id, SEL, int))objc_msgSend)(self, loadSelector, 0); + }); + }); + static char kReloadKey; if (objc_getAssociatedObject(self, &kReloadKey)) return; objc_setAssociatedObject(self, &kReloadKey, @YES, diff --git a/WQFilzaIntegration.h b/WQFilzaIntegration.h index 30cbe95..cb78c48 100644 --- a/WQFilzaIntegration.h +++ b/WQFilzaIntegration.h @@ -53,3 +53,6 @@ FOUNDATION_EXPORT NSArray * _Nonnull WQChildrenForRoot(NSString * _N FOUNDATION_EXPORT NSString *_Nullable WQIdentifyContainerPath(NSString * _Nonnull path); FOUNDATION_EXPORT NSArray * _Nonnull WQActiveGrantHandles(void); FOUNDATION_EXPORT NSString * _Nonnull WQFallbackDisplayName(NSString * _Nonnull bundleID); +/// Clears the cached app list and requests a fresh per-container traversal the +/// next time the App Manager asks for applications. +FOUNDATION_EXPORT void WQRequestAppManagerRescan(void); diff --git a/WQFilzaIntegration.m b/WQFilzaIntegration.m index f5b0efc..920aebe 100644 --- a/WQFilzaIntegration.m +++ b/WQFilzaIntegration.m @@ -50,6 +50,30 @@ static NSMutableDictionary *gDataMap; static BOOL gBundleMapAttempted; static NSObject *gBundleMapLock; static dispatch_queue_t gAppsQueue; +static BOOL gBuildingApps; +static void *kWQAppsQueueKey = &kWQAppsQueueKey; + +static dispatch_queue_t WQAppsQueue(void) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + gAppsQueue = dispatch_queue_create( + "com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL); + dispatch_queue_set_specific(gAppsQueue, kWQAppsQueueKey, + (void *)1, NULL); + }); + return gAppsQueue; +} + +static void WQAppsQueueSync(dispatch_block_t block) +{ + if (!block) return; + if (dispatch_get_specific(kWQAppsQueueKey)) { + block(); + return; + } + dispatch_sync(WQAppsQueue(), block); +} static dispatch_queue_t gFamiliesQueue; static NSMutableDictionary *> *gPathleakCache; static dispatch_group_t gStartupGroup; @@ -358,9 +382,7 @@ void WQRefreshDiscovery(void) { dispatch_async(gFamiliesQueue ?: dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ WQDiscoverPerContainerFamilies(); - if (!gAppsQueue) gAppsQueue = dispatch_queue_create( - "com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL); - dispatch_sync(gAppsQueue, ^{ + WQAppsQueueSync(^{ gAppsCache = WQBuildAppList(); gAppsCacheDate = [NSDate date]; }); @@ -1123,6 +1145,143 @@ static void WQAppendMobileInstallationApps(NSMutableArray *result, (unsigned long)apps.count, (unsigned long)result.count); } +static BOOL gForceAppTraversal; + +#pragma mark - Per-entry app scan + +static void WQAddCandidateBundleID(NSMutableOrderedSet *ids, + id value) +{ + if ([value isKindOfClass:NSString.class] && + WQLooksLikeBundleIdentifier(value)) + [ids addObject:value]; +} + +static void WQAddProxyBundleID(NSMutableOrderedSet *ids, id proxy) +{ + if (!proxy) return; + NSArray *selectors = @[ + @"applicationIdentifier", @"bundleIdentifier", @"bundleId", + @"applicationID", + ]; + for (NSString *name in selectors) { + SEL selector = NSSelectorFromString(name); + if (![proxy respondsToSelector:selector]) continue; + id value = ((id(*)(id, SEL))objc_msgSend)(proxy, selector); + WQAddCandidateBundleID(ids, value); + } + for (NSString *key in @[@"bundleIdentifier", @"applicationIdentifier", + @"_bundleIdentifier", @"_applicationIdentifier"]) { + @try { + WQAddCandidateBundleID(ids, [proxy valueForKey:key]); + } @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]) { + NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile: + [[(NSURL *)url path] stringByAppendingPathComponent:@"Info.plist"]]; + WQAddCandidateBundleID(ids, plist[@"CFBundleIdentifier"]); + } + } +} + +/// Bundle ids exposed by LaunchServices without relying on allApplications. +/// This is the fallback when the CSStore parser/layout is unavailable. +static NSArray *WQCandidateBundleIDs(void) +{ + NSMutableOrderedSet *ids = [NSMutableOrderedSet orderedSet]; + Class workspaceClass = NSClassFromString(@"LSApplicationWorkspace"); + id workspace = nil; + SEL defaultSelector = NSSelectorFromString(@"defaultWorkspace"); + if ([workspaceClass respondsToSelector:defaultSelector]) + workspace = ((id(*)(id, SEL))objc_msgSend)(workspaceClass, + defaultSelector); + + NSArray *arraySelectors = @[ + @"installedApplications", @"allInstalledApplications", + @"allApplications", @"unrestrictedApplications", + @"directionsApplications", @"placeholderApplications", + ]; + if (workspace) { + 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]) continue; + for (id item in value) { + if ([item isKindOfClass:NSString.class]) + WQAddCandidateBundleID(ids, item); + else + WQAddProxyBundleID(ids, item); + } + } + + 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]) continue; + for (id item in value) WQAddProxyBundleID(ids, item); + } + } + + SEL enumerateSelector = NSSelectorFromString( + @"enumerateApplicationsOfType:block:"); + if ([workspace respondsToSelector:enumerateSelector]) { + void (^block)(id) = ^(id proxy) { + WQAddProxyBundleID(ids, proxy); + }; + ((void(*)(id, SEL, NSUInteger, id))objc_msgSend)(workspace, + enumerateSelector, 0, block); + } + } + + // MobileInstallation is absent on some iOS 27 builds, but present on + // others; use it when available. + NSDictionary *installation = + WQMobileInstallationApps(); + for (NSString *key in installation) { + WQAddCandidateBundleID(ids, key); + NSDictionary *entry = installation[key]; + if ([entry isKindOfClass:NSDictionary.class]) + WQAddCandidateBundleID(ids, entry[@"CFBundleIdentifier"]); + } + return ids.array; +} + +static NSArray *WQContainersForBundleIDs(NSArray *ids) +{ + NSMutableArray *paths = [NSMutableArray array]; + for (NSString *bundleID in ids) { + if (!bundleID.length) continue; + NSString *error = nil; + NSString *path = MCMFilzaDataContainerPath(bundleID, &error); + if (!path.length) continue; + path = WQCanonicalPath(path); + if (path.length && WQPathExists(path)) [paths addObject:path]; + } + return paths; +} + +void WQRequestAppManagerRescan(void) +{ + gForceAppTraversal = YES; + WQAppsQueueSync(^{ + gAppsCache = nil; + gAppsCacheDate = nil; + }); + if (gPathleakCache) [gPathleakCache removeAllObjects]; + NSLog(@"[WQ] App Manager requested a fresh per-container scan"); +} + static NSArray *> *WQBuildAppList(void) { NSString *root = @"/private/var/mobile/Containers/Data/Application"; @@ -1133,27 +1292,44 @@ static NSArray *> *WQBuildAppList(void) WQPostStartupProgress(0.20, @"正在读取 LaunchServices 应用数据库…"); WQLoadLaunchServicesDatabase(); - NSMutableArray *lsContainers = [NSMutableArray array]; + BOOL forceTraversal = gForceAppTraversal; + gForceAppTraversal = NO; + NSMutableArray *candidates = [NSMutableArray array]; + + // Traditional path traversal: whenever the App Manager opens, enumerate + // the data-container root again through worse_query/pathleak. This does + // not depend on the LaunchServices database being readable. + if (forceTraversal) { + WQPostStartupProgress(0.24, @"正在逐个遍历应用数据容器…"); + [candidates addObjectsFromArray:WQEnumerateContainers(root)]; + } + + // Names/container hints from the LaunchServices database. These are a + // supplement, never the only source. + [candidates addObjectsFromArray:WQLSContainerPaths() ?: @[]]; + + // Per-app container lookup by bundle id. This is the fallback for devices + // where the data-container root cannot be listed at all. + NSArray *bundleIDs = WQCandidateBundleIDs(); + [candidates addObjectsFromArray:WQContainersForBundleIDs(bundleIDs)]; + + // If neither source found anything, do the traditional enumeration even + // when this was not an App Manager-triggered scan. + if (candidates.count == 0) { + [candidates addObjectsFromArray:WQEnumerateContainers(root)]; + } + NSString *lsPrefix = [root stringByAppendingString:@"/"]; - for (NSString *path in WQLSContainerPaths() ?: @[]) { + NSMutableOrderedSet *unique = [NSMutableOrderedSet orderedSet]; + for (NSString *path in candidates) { NSString *canonical = WQCanonicalPath(path); if (![canonical hasPrefix:lsPrefix]) continue; if (canonical.length != lsPrefix.length + 36) continue; if (![[NSUUID alloc] initWithUUIDString: [canonical substringFromIndex:lsPrefix.length]]) continue; - [lsContainers addObject:canonical]; - } - - NSArray *containers = nil; - if (lsContainers.count >= 2) { - containers = lsContainers; - WQPostStartupProgress(0.30, [NSString stringWithFormat: - @"应用数据库已列出 %lu 个数据容器,正在授权…", - (unsigned long)containers.count]); - } else { - WQPostStartupProgress(0.26, @"正在扫描应用数据容器…"); - containers = WQEnumerateContainers(root); + [unique addObject:canonical]; } + NSArray *containers = unique.array; WQPostStartupProgress(0.40, [NSString stringWithFormat: @"发现 %lu 个数据容器,正在逐个授权…", (unsigned long)containers.count]); NSUInteger granted = 0; @@ -1176,17 +1352,21 @@ static NSArray *> *WQBuildAppList(void) NSArray *> *WQInstalledApplications(void) { - if (!gAppsQueue) gAppsQueue = dispatch_queue_create( - "com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL); __block NSArray *value = nil; - dispatch_sync(gAppsQueue, ^{ + WQAppsQueueSync(^{ NSTimeInterval age = gAppsCacheDate ? -[gAppsCacheDate timeIntervalSinceNow] : 1e9; + if (gBuildingApps) { + value = gAppsCache ?: @[]; + return; + } // 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) { + gBuildingApps = YES; gAppsCache = WQBuildAppList(); gAppsCacheDate = [NSDate date]; + gBuildingApps = NO; } value = gAppsCache; }); @@ -1507,15 +1687,14 @@ void WQFilzaStart(void) WQScheduleFavorites(); // Build the app-container index before the user opens Apps manager. - if (!gAppsQueue) gAppsQueue = dispatch_queue_create( - "com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL); + WQAppsQueue(); if (!gFamiliesQueue) gFamiliesQueue = dispatch_queue_create( "com.filzaslop.worsequery.families", DISPATCH_QUEUE_SERIAL); // App containers are the priority: the Apps manager must populate before // the slower system-group sweep begins. dispatch_group_enter(gStartupGroup); - dispatch_async(gAppsQueue, ^{ + dispatch_async(WQAppsQueue(), ^{ WQPostStartupProgress(0.24, @"正在枚举应用数据容器…"); if (!gAppsCache) gAppsCache = WQBuildAppList(); gAppsCacheDate = [NSDate date]; @@ -1587,6 +1766,18 @@ void WQFilzaStart(void) - (NSNumber *)dynamicDiskUsage { return @0; } - (BOOL)isDeletable { return YES; } - (BOOL)isPlaceholder { return NO; } -- (BOOL)isSystem { return NO; } +- (BOOL)isSystem { + return [_bundleIdentifier hasPrefix:@"com.apple."]; +} +- (BOOL)isInstalled { return YES; } +- (BOOL)isNewsstandApp { return NO; } +- (BOOL)isRestricted { return NO; } +- (BOOL)isLaunchProhibited { return NO; } +- (BOOL)isHidden { return NO; } +- (NSString *)minimumSystemVersion { return @"15.0"; } +- (NSString *)teamID { return @""; } +- (long long)bundleModTime { + return (long long)NSDate.date.timeIntervalSinceReferenceDate; +} @end