diff --git a/AppsManager_iOS27.ipa b/AppsManager_iOS27.ipa index 42e6f1d..9e16fd5 100644 Binary files a/AppsManager_iOS27.ipa and b/AppsManager_iOS27.ipa differ diff --git a/README.md b/README.md index 6a0c141..c804989 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - 源码 / Source:[source/](source/)(iOS 27 Hook、内置文件浏览器集成与构建脚本 / iOS 27 hook, file-browser integration and build scripts) ```text -98ff0ad0128b107d1f1a21583d9d6e28617f3491ba4ab923a15312fc8e0c14d4 AppsManager_iOS27.ipa +f33f847a203fde8cb30e659406e2d72e65eb12aaab0de2fb6665350d68e60275 AppsManager_iOS27.ipa ``` ## 功能 / Features @@ -67,6 +67,12 @@ ## 更新说明 / Updates +- 应用列表改为每次启动通过 `worse_query` 遍历 `Data/Application` 容器生成,不再依赖 LaunchServices CSStore 解析成功才能显示列表。 + The app list is now built by traversing `Data/Application` containers through `worse_query` on each launch; a readable LaunchServices CSStore is no longer required. +- App 名称依次从原生 LaunchServices API、App bundle 的 `Info.plist`、LaunchServices 显示名获取,最后回退到 bundle ID。 + App names come from the native LaunchServices API, the app bundle's `Info.plist`, then LaunchServices display names, with the bundle ID as the final fallback. +- 首次全量路径扫描约 30 秒,会显示进度条;后续启动使用增量扫描缓存。 + The first full path scan takes about 30 seconds and shows progress; later launches use the incremental scan cache. - 修复首次启动空白、一次性「开发者提示」遮挡列表的问题。 Fixed the blank first launch and the one-time developer alert covering the list. - iOS 27 Hook 已移入 `Frameworks/AppsManagerHook.framework`,标准签名工具重签时会把它作为嵌套代码一并签名。 @@ -94,5 +100,5 @@ ## 校验 / Checksum ```text -98ff0ad0128b107d1f1a21583d9d6e28617f3491ba4ab923a15312fc8e0c14d4 AppsManager_iOS27.ipa +f33f847a203fde8cb30e659406e2d72e65eb12aaab0de2fb6665350d68e60275 AppsManager_iOS27.ipa ``` diff --git a/SHA256SUMS.txt b/SHA256SUMS.txt index 1bba93d..69d276b 100644 --- a/SHA256SUMS.txt +++ b/SHA256SUMS.txt @@ -1 +1 @@ -98ff0ad0128b107d1f1a21583d9d6e28617f3491ba4ab923a15312fc8e0c14d4 AppsManager_iOS27.ipa +f33f847a203fde8cb30e659406e2d72e65eb12aaab0de2fb6665350d68e60275 AppsManager_iOS27.ipa diff --git a/source/README.md b/source/README.md index 995433e..6b983ed 100644 --- a/source/README.md +++ b/source/README.md @@ -33,6 +33,20 @@ source/ - Detail page version/path/date/app-group support. - Replace the `filza://` prompt with the built-in WQ Files browser. - Present media through the native `AVPlayerViewController`. +- Build the app list by traversing `/private/var/mobile/Containers/Data/Application` through `worse_query` on every launch. LaunchServices is optional and only used for names and bundle paths. +- Resolve app names from the native LaunchServices API, the bundle `Info.plist`, LaunchServices display names, and finally a prettified bundle ID. +- Rebuild the list after preparation if the first load ran before the container registry was ready. + +## 应用列表 / App list + +The list no longer requires a readable LaunchServices CSStore: + +1. Enumerate app data containers through `wq_enumerate` (MCM / LaunchServices API / full inode scan fallback). +2. Resolve each container's bundle ID from the native API, LaunchServices map, container xattr, metadata plist, or bundle ID resolver. +3. Resolve the display name from the native API, bundle `Info.plist`, LaunchServices display names, or the bundle ID. +4. If the screen loaded before preparation finished, reload it once the registry is ready. + +The first full scan on a device where the LaunchServices API and container manager return nothing can take around 30 seconds; the launch progress sheet is shown. Later launches reuse the incremental scan cache. ## 构建要求 / Build requirements diff --git a/source/hook/src/WBHook.m b/source/hook/src/WBHook.m index cd18adc..e68202b 100644 --- a/source/hook/src/WBHook.m +++ b/source/hook/src/WBHook.m @@ -355,6 +355,14 @@ static NSString *WBLocalized(NSString *key) @property (nonatomic, strong) NSMutableDictionary *> *groupCache; @end +@protocol WBLProxyInfo +- (NSString *)bundleIdentifier; +- (NSString *)localizedName; +- (NSURL *)bundleURL; +- (NSURL *)dataContainerURL; +- (NSString *)shortVersionString; +@end + @implementation WBRegistry + (instancetype)shared @@ -610,6 +618,58 @@ static NSString *WBFindStoreWithProgress(void (^progress)(double, NSString *)) return nil; } +static NSArray *(*gOrigAllInstalled)(id, SEL); + +static NSDictionary *WBReadInfoPlist(NSString *bundlePath) +{ + if (!bundlePath.length) return nil; + NSString *path = [bundlePath stringByAppendingPathComponent:@"Info.plist"]; + NSData *data = wq_read(path, 2 * 1024 * 1024, NULL); + if (!data) { + wq_grant(bundlePath); + data = wq_read(path, 2 * 1024 * 1024, NULL); + } + if (!data) return nil; + id plist = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:nil]; + return [plist isKindOfClass:NSDictionary.class] ? plist : nil; +} + +static NSString *WBInfoString(NSDictionary *info, NSArray *keys) +{ + for (NSString *key in keys) { + id value = info[key]; + if ([value isKindOfClass:NSString.class] && [value length]) return value; + } + return nil; +} + +static NSString *WBBundleIDFromContainerMetadata(NSString *container) +{ + NSString *path = [container stringByAppendingPathComponent:@".com.apple.mobile_container_manager.metadata.plist"]; + wq_grant(container); + wq_grant(path); + NSData *data = wq_read(path, 256 * 1024, NULL); + if (!data) return nil; + id plist = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:nil]; + id identifier = [plist isKindOfClass:NSDictionary.class] ? plist[@"MCMMetadataIdentifier"] : nil; + return [identifier isKindOfClass:NSString.class] ? identifier : nil; +} + +static NSArray *WBOriginalInstalledApplications(void) +{ + Class workspaceClass = NSClassFromString(@"LSApplicationWorkspace"); + if (!workspaceClass || !gOrigAllInstalled) return nil; + id workspace = [workspaceClass respondsToSelector:@selector(defaultWorkspace)] + ? [workspaceClass performSelector:@selector(defaultWorkspace)] : nil; + if (!workspace) return nil; + @try { + return gOrigAllInstalled(workspace, NSSelectorFromString(@"allInstalledApplications")); + } @catch (__unused NSException *exception) { + return nil; + } +} + + - (void)prepareWithProgress:(void (^)(double, NSString *))progress completion:(void (^)(BOOL, NSString *))completion { @@ -620,19 +680,77 @@ static NSString *WBFindStoreWithProgress(void (^progress)(double, NSString *)) if (completion) completion(NO, WBLocalized(@"no_group")); return; } + + // LaunchServices is optional enrichment: names, bundle paths, and a + // container->bundle-id map. The list itself comes from path traversal + // below, so a missing or unreadable store no longer produces blank UI. if (progress) progress(0.08, WBLocalized(@"locating_ls")); NSString *store = WBFindStoreWithProgress(progress); - if (!store.length) { - if (completion) completion(NO, WBLocalized(@"no_ls")); - return; + NSMutableDictionary *lsContainerToID = [NSMutableDictionary new]; + if (store.length) { + [NSUserDefaults.standardUserDefaults setObject:store forKey:@"WQLaunchServicesStore"]; + [NSUserDefaults.standardUserDefaults synchronize]; + wq_display_names_reset_miss(); + wq_load_display_names(); + for (NSString *path in wq_ls_container_paths(store)) { + NSString *bundleID = wq_bundle_id_for_container(path); + if (bundleID.length) lsContainerToID[path] = bundleID; + } } - if (progress) progress(0.40, WBLocalized(@"parsing_apps")); - [NSUserDefaults.standardUserDefaults setObject:store forKey:@"WQLaunchServicesStore"]; - [NSUserDefaults.standardUserDefaults synchronize]; - NSArray *containers = wq_ls_container_paths(store); - wq_display_names_reset_miss(); - wq_load_display_names(); - if (!containers.count) { + + // The native LaunchServices API is independent of the CSStore parser + // and still provides display names and bundle paths when the store is + // missing or unreadable. + NSMutableDictionary *apiName = [NSMutableDictionary new]; + NSMutableDictionary *apiBundle = [NSMutableDictionary new]; + NSMutableDictionary *apiVersion = [NSMutableDictionary new]; + NSMutableDictionary *apiContainerToID = [NSMutableDictionary new]; + for (id proxy in WBOriginalInstalledApplications()) { + NSString *bundleID = [proxy bundleIdentifier]; + if (!bundleID.length) continue; + NSString *name = [proxy localizedName]; + if (name.length) apiName[bundleID] = name; + NSURL *bundleURL = [proxy bundleURL]; + if (bundleURL.path.length) apiBundle[bundleID] = bundleURL.path; + NSString *version = [proxy shortVersionString]; + if (version.length && ![version isEqualToString:@"null"]) apiVersion[bundleID] = version; + NSURL *dataURL = [proxy dataContainerURL]; + if (dataURL.path.length) apiContainerToID[dataURL.path] = bundleID; + } + + // Enumerate app data containers directly through worse_query on every + // launch. This does not depend on the LaunchServices database being + // readable, which is the failure mode on affected devices. + if (progress) progress(0.25, WBLocalized(@"scanning_containers")); + NSString *root = @"/private/var/mobile/Containers/Data/Application"; + wq_set_full_scan_ui(WBScanConfirmC, WBScanProgressC); + gWBScanProgressBlock = ^(int64_t done, int64_t total, BOOL __unused finished) { + if (progress && total > 0) { + progress(0.25 + 0.35 * ((double)done / (double)total), WBLocalized(@"scanning_containers_short")); + } + }; + NSArray *enumerated = wq_enumerate(root, 0, nil, ^(int64_t done, int64_t total, NSUInteger __unused count) { + if (progress && total > 0) { + progress(0.25 + 0.35 * ((double)done / (double)total), WBLocalized(@"scanning_containers_short")); + } + }); + gWBScanProgressBlock = nil; + NSLog(@"[WB] app containers: %lu enumerated, %lu from LaunchServices API", + (unsigned long)enumerated.count, (unsigned long)apiContainerToID.count); + + NSMutableOrderedSet *containerSet = [NSMutableOrderedSet orderedSet]; + for (NSString *path in enumerated) { + if ([path.stringByDeletingLastPathComponent isEqualToString:root]) [containerSet addObject:path]; + } + for (NSString *path in apiContainerToID.allKeys) { + if ([path.stringByDeletingLastPathComponent isEqualToString:root]) [containerSet addObject:path]; + } + for (NSString *path in lsContainerToID.allKeys) { + if ([path.stringByDeletingLastPathComponent isEqualToString:root]) [containerSet addObject:path]; + } + NSLog(@"[WB] containers: %lu enumerated, %lu API, %lu LaunchServices maps", + (unsigned long)enumerated.count, (unsigned long)apiContainerToID.count, (unsigned long)lsContainerToID.count); + if (!containerSet.count) { if (completion) completion(NO, WBLocalized(@"no_containers")); return; } @@ -641,30 +759,57 @@ static NSString *WBFindStoreWithProgress(void (^progress)(double, NSString *)) NSMutableDictionary *byID = [NSMutableDictionary new]; NSMutableArray *apps = [NSMutableArray new]; NSMutableArray *proxies = [NSMutableArray new]; - for (NSString *container in containers) { - if (![container containsString:@"/Data/Application/"]) continue; - NSString *bundleID = wq_bundle_id_for_container(container); - if (!bundleID.length || byID[bundleID]) continue; - NSString *bundlePath = wq_bundle_path_for_id(bundleID); - if (!bundlePath.length || ![bundlePath.pathExtension isEqualToString:@"app"]) continue; - NSString *parent = bundlePath.stringByDeletingLastPathComponent; - if (![parent.stringByDeletingLastPathComponent isEqualToString:@"/private/var/containers/Bundle/Application"]) continue; + NSMutableDictionary *infoCache = [NSMutableDictionary new]; + NSUInteger missingLabels = 0; + for (NSString *container in containerSet) { + NSString *bundleID = apiContainerToID[container]; + if (!bundleID.length) bundleID = lsContainerToID[container]; + if (!bundleID.length) bundleID = wq_container_label(container); + if (!bundleID.length) bundleID = WBBundleIDFromContainerMetadata(container); + if (!bundleID.length) bundleID = wq_bundle_id_for_container(container); + if (!bundleID.length || byID[bundleID]) { + if (!bundleID.length) missingLabels++; + continue; + } + + NSString *bundlePath = apiBundle[bundleID]; + if (!bundlePath.length) bundlePath = wq_bundle_path_for_id(bundleID); + NSDictionary *info = nil; + if (bundlePath.length) { + info = infoCache[bundlePath]; + if (!info) { + info = WBReadInfoPlist(bundlePath); + if (info) infoCache[bundlePath] = info; + } + } + NSString *name = apiName[bundleID]; + if (!name.length) name = WBInfoString(info, @[@"CFBundleDisplayName", @"CFBundleName", @"CFBundleExecutable"]); + if (!name.length) name = wq_display_name(bundleID); + if (!name.length) name = WBPrettifyBundleID(bundleID); WBAppInfo *app = [WBAppInfo new]; app.bundleID = bundleID; app.containerPath = container; app.bundlePath = bundlePath; + app.version = apiVersion[bundleID] ?: WBInfoString(info, @[@"CFBundleShortVersionString", @"CFBundleVersion"]); app.isSystem = [bundleID hasPrefix:@"com.apple."]; - NSString *name = wq_display_name(bundleID); - app.name = name.length ? name : WBPrettifyBundleID(bundleID); + app.name = name; byID[bundleID] = app; [apps addObject:app]; [proxies addObject:[[WBAppProxy alloc] initWithInfo:app]]; } + + if (!apps.count) { + NSLog(@"[WB] built 0 apps from %lu containers; %lu without bundle id", + (unsigned long)containerSet.count, (unsigned long)missingLabels); + if (completion) completion(NO, WBLocalized(@"no_containers")); + return; + } + NSLog(@"[WB] built %lu apps from %lu containers; %lu without bundle id", + (unsigned long)apps.count, (unsigned long)containerSet.count, (unsigned long)missingLabels); [apps sortUsingComparator:^NSComparisonResult(WBAppInfo *a, WBAppInfo *b) { return [a.name localizedStandardCompare:b.name]; }]; - // Rebuild proxies in sorted order. [proxies removeAllObjects]; for (WBAppInfo *app in apps) [proxies addObject:[[WBAppProxy alloc] initWithInfo:app]]; @@ -773,7 +918,6 @@ static NSString *WBFindStoreWithProgress(void (^progress)(double, NSString *)) #pragma mark - Hooks -static NSArray *(*gOrigAllInstalled)(id, SEL); static NSArray *(*gOrigAllApplications)(id, SEL); static id (*gOrigProxyForIdentifier)(id, SEL, NSString *); static NSString *(*gOrigDataPath)(id, SEL); @@ -822,6 +966,7 @@ static BOOL WBWaitForRegistry(void) static NSArray *WBHookAllInstalled(id self, SEL _cmd) { + if (gPreparing) return gOrigAllInstalled ? gOrigAllInstalled(self, _cmd) : @[]; if (!WBRegistry.shared.ready && !NSThread.isMainThread) WBWaitForRegistry(); NSArray *proxies = WBRegistry.shared.proxies; if (WBRegistry.shared.ready && proxies.count) return proxies; @@ -830,6 +975,7 @@ static NSArray *WBHookAllInstalled(id self, SEL _cmd) static NSArray *WBHookAllApplications(id self, SEL _cmd) { + if (gPreparing) return gOrigAllApplications ? gOrigAllApplications(self, _cmd) : @[]; if (!WBRegistry.shared.ready && !NSThread.isMainThread) WBWaitForRegistry(); NSArray *proxies = WBRegistry.shared.proxies; if (WBRegistry.shared.ready && proxies.count) return proxies; @@ -1711,6 +1857,7 @@ static void WBStartPreparation(void) // Last-resort fallback: start even without a presenter. gPreparing = YES; [WBRegistry.shared prepareWithProgress:nil completion:^(BOOL ok, NSString *message) { + gPreparing = NO; gPrepared = YES; [gReadyCondition lock]; [gReadyCondition broadcast]; @@ -1732,6 +1879,7 @@ static void WBStartPreparation(void) [WBRegistry.shared prepareWithProgress:^(double fraction, NSString *status) { [gProgressController updateFraction:fraction status:status]; } completion:^(BOOL ok, NSString *message) { + gPreparing = NO; gPrepared = YES; [gReadyCondition lock]; [gReadyCondition broadcast]; @@ -1741,6 +1889,17 @@ static void WBStartPreparation(void) [gProgressController dismissViewControllerAnimated:YES completion:nil]; gProgressController = nil; } + // The first load may have started before preparation finished and + // received an empty native list. Rebuild it now that the registry + // is ready so the path-enumerated apps actually appear. + id appsVC = gAppsTableViewController; + if (appsVC) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 200 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + if ([appsVC respondsToSelector:@selector(reload)]) { + [(id)appsVC reload]; + } + }); + } if (!ok && message.length) { NSString *detail = message; if ([message containsString:@"App Group"]) {