App Manager: per-entry traversal fallback and stable list
This commit is contained in:
1 parent
0e5851c521
commit
dc51a3a3c7
4 files changed
+234
-24
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -53,3 +53,6 @@ FOUNDATION_EXPORT NSArray<NSString *> * _Nonnull WQChildrenForRoot(NSString * _N
|
||||
FOUNDATION_EXPORT NSString *_Nullable WQIdentifyContainerPath(NSString * _Nonnull path);
|
||||
FOUNDATION_EXPORT NSArray<NSNumber *> * _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);
|
||||
+215
-24
@@ -50,6 +50,30 @@ static NSMutableDictionary<NSString *, NSString *> *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<NSString *, NSArray<NSString *> *> *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<NSString *> *ids,
|
||||
id value)
|
||||
{
|
||||
if ([value isKindOfClass:NSString.class] &&
|
||||
WQLooksLikeBundleIdentifier(value))
|
||||
[ids addObject:value];
|
||||
}
|
||||
|
||||
static void WQAddProxyBundleID(NSMutableOrderedSet<NSString *> *ids, id proxy)
|
||||
{
|
||||
if (!proxy) return;
|
||||
NSArray<NSString *> *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<NSString *> *WQCandidateBundleIDs(void)
|
||||
{
|
||||
NSMutableOrderedSet<NSString *> *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<NSString *> *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<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]) 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<NSString *, 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<NSString *> *WQContainersForBundleIDs(NSArray<NSString *> *ids)
|
||||
{
|
||||
NSMutableArray<NSString *> *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<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
|
||||
{
|
||||
NSString *root = @"/private/var/mobile/Containers/Data/Application";
|
||||
@@ -1133,27 +1292,44 @@ static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
|
||||
WQPostStartupProgress(0.20, @"正在读取 LaunchServices 应用数据库…");
|
||||
WQLoadLaunchServicesDatabase();
|
||||
|
||||
NSMutableArray<NSString *> *lsContainers = [NSMutableArray array];
|
||||
BOOL forceTraversal = gForceAppTraversal;
|
||||
gForceAppTraversal = NO;
|
||||
NSMutableArray<NSString *> *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<NSString *> *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<NSString *> *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<NSString *> *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<NSString *> *containers = unique.array;
|
||||
WQPostStartupProgress(0.40, [NSString stringWithFormat:
|
||||
@"发现 %lu 个数据容器,正在逐个授权…", (unsigned long)containers.count]);
|
||||
NSUInteger granted = 0;
|
||||
@@ -1176,17 +1352,21 @@ static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
|
||||
|
||||
NSArray<NSDictionary<NSString *, NSString *> *> *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
|
||||
Reference in new issue
Block a user