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
|
- App discovery uses the LaunchServices container list instead of a full
|
||||||
inode scan on supported versions, so the first launch and App Manager open
|
inode scan on supported versions, so the first launch and App Manager open
|
||||||
much faster.
|
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-`
|
- App icons are used where iOS returns them (`_applicationIconImageForBundle-`
|
||||||
`Identifier:format:scale:`); otherwise Filza's placeholder is shown.
|
`Identifier:format:scale:`); otherwise Filza's placeholder is shown.
|
||||||
- Accessible roots, App Data (by app), App Data (UUID), App Groups and System
|
- 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)
|
if (orig_appsViewDidAppear)
|
||||||
((void(*)(id, SEL, BOOL))orig_appsViewDidAppear)(self, _cmd, animated);
|
((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;
|
static char kReloadKey;
|
||||||
if (objc_getAssociatedObject(self, &kReloadKey)) return;
|
if (objc_getAssociatedObject(self, &kReloadKey)) return;
|
||||||
objc_setAssociatedObject(self, &kReloadKey, @YES,
|
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 NSString *_Nullable WQIdentifyContainerPath(NSString * _Nonnull path);
|
||||||
FOUNDATION_EXPORT NSArray<NSNumber *> * _Nonnull WQActiveGrantHandles(void);
|
FOUNDATION_EXPORT NSArray<NSNumber *> * _Nonnull WQActiveGrantHandles(void);
|
||||||
FOUNDATION_EXPORT NSString * _Nonnull WQFallbackDisplayName(NSString * _Nonnull bundleID);
|
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 BOOL gBundleMapAttempted;
|
||||||
static NSObject *gBundleMapLock;
|
static NSObject *gBundleMapLock;
|
||||||
static dispatch_queue_t gAppsQueue;
|
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 dispatch_queue_t gFamiliesQueue;
|
||||||
static NSMutableDictionary<NSString *, NSArray<NSString *> *> *gPathleakCache;
|
static NSMutableDictionary<NSString *, NSArray<NSString *> *> *gPathleakCache;
|
||||||
static dispatch_group_t gStartupGroup;
|
static dispatch_group_t gStartupGroup;
|
||||||
@@ -358,9 +382,7 @@ void WQRefreshDiscovery(void)
|
|||||||
{
|
{
|
||||||
dispatch_async(gFamiliesQueue ?: dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
dispatch_async(gFamiliesQueue ?: dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||||||
WQDiscoverPerContainerFamilies();
|
WQDiscoverPerContainerFamilies();
|
||||||
if (!gAppsQueue) gAppsQueue = dispatch_queue_create(
|
WQAppsQueueSync(^{
|
||||||
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
|
|
||||||
dispatch_sync(gAppsQueue, ^{
|
|
||||||
gAppsCache = WQBuildAppList();
|
gAppsCache = WQBuildAppList();
|
||||||
gAppsCacheDate = [NSDate date];
|
gAppsCacheDate = [NSDate date];
|
||||||
});
|
});
|
||||||
@@ -1123,6 +1145,143 @@ static void WQAppendMobileInstallationApps(NSMutableArray *result,
|
|||||||
(unsigned long)apps.count, (unsigned long)result.count);
|
(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)
|
static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
|
||||||
{
|
{
|
||||||
NSString *root = @"/private/var/mobile/Containers/Data/Application";
|
NSString *root = @"/private/var/mobile/Containers/Data/Application";
|
||||||
@@ -1133,27 +1292,44 @@ static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
|
|||||||
WQPostStartupProgress(0.20, @"正在读取 LaunchServices 应用数据库…");
|
WQPostStartupProgress(0.20, @"正在读取 LaunchServices 应用数据库…");
|
||||||
WQLoadLaunchServicesDatabase();
|
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:@"/"];
|
NSString *lsPrefix = [root stringByAppendingString:@"/"];
|
||||||
for (NSString *path in WQLSContainerPaths() ?: @[]) {
|
NSMutableOrderedSet<NSString *> *unique = [NSMutableOrderedSet orderedSet];
|
||||||
|
for (NSString *path in candidates) {
|
||||||
NSString *canonical = WQCanonicalPath(path);
|
NSString *canonical = WQCanonicalPath(path);
|
||||||
if (![canonical hasPrefix:lsPrefix]) continue;
|
if (![canonical hasPrefix:lsPrefix]) continue;
|
||||||
if (canonical.length != lsPrefix.length + 36) continue;
|
if (canonical.length != lsPrefix.length + 36) continue;
|
||||||
if (![[NSUUID alloc] initWithUUIDString:
|
if (![[NSUUID alloc] initWithUUIDString:
|
||||||
[canonical substringFromIndex:lsPrefix.length]]) continue;
|
[canonical substringFromIndex:lsPrefix.length]]) continue;
|
||||||
[lsContainers addObject:canonical];
|
[unique 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);
|
|
||||||
}
|
}
|
||||||
|
NSArray<NSString *> *containers = unique.array;
|
||||||
WQPostStartupProgress(0.40, [NSString stringWithFormat:
|
WQPostStartupProgress(0.40, [NSString stringWithFormat:
|
||||||
@"发现 %lu 个数据容器,正在逐个授权…", (unsigned long)containers.count]);
|
@"发现 %lu 个数据容器,正在逐个授权…", (unsigned long)containers.count]);
|
||||||
NSUInteger granted = 0;
|
NSUInteger granted = 0;
|
||||||
@@ -1176,17 +1352,21 @@ static NSArray<NSDictionary<NSString *, NSString *> *> *WQBuildAppList(void)
|
|||||||
|
|
||||||
NSArray<NSDictionary<NSString *, NSString *> *> *WQInstalledApplications(void)
|
NSArray<NSDictionary<NSString *, NSString *> *> *WQInstalledApplications(void)
|
||||||
{
|
{
|
||||||
if (!gAppsQueue) gAppsQueue = dispatch_queue_create(
|
|
||||||
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
|
|
||||||
__block NSArray *value = nil;
|
__block NSArray *value = nil;
|
||||||
dispatch_sync(gAppsQueue, ^{
|
WQAppsQueueSync(^{
|
||||||
NSTimeInterval age = gAppsCacheDate
|
NSTimeInterval age = gAppsCacheDate
|
||||||
? -[gAppsCacheDate timeIntervalSinceNow] : 1e9;
|
? -[gAppsCacheDate timeIntervalSinceNow] : 1e9;
|
||||||
|
if (gBuildingApps) {
|
||||||
|
value = gAppsCache ?: @[];
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Never cache an empty result: the first call can happen before the
|
// Never cache an empty result: the first call can happen before the
|
||||||
// worse_query grants and app-container scan have finished.
|
// worse_query grants and app-container scan have finished.
|
||||||
if (!gAppsCache || gAppsCache.count == 0 || age > 120) {
|
if (!gAppsCache || gAppsCache.count == 0 || age > 120) {
|
||||||
|
gBuildingApps = YES;
|
||||||
gAppsCache = WQBuildAppList();
|
gAppsCache = WQBuildAppList();
|
||||||
gAppsCacheDate = [NSDate date];
|
gAppsCacheDate = [NSDate date];
|
||||||
|
gBuildingApps = NO;
|
||||||
}
|
}
|
||||||
value = gAppsCache;
|
value = gAppsCache;
|
||||||
});
|
});
|
||||||
@@ -1507,15 +1687,14 @@ void WQFilzaStart(void)
|
|||||||
WQScheduleFavorites();
|
WQScheduleFavorites();
|
||||||
|
|
||||||
// Build the app-container index before the user opens Apps manager.
|
// Build the app-container index before the user opens Apps manager.
|
||||||
if (!gAppsQueue) gAppsQueue = dispatch_queue_create(
|
WQAppsQueue();
|
||||||
"com.filzaslop.worsequery.apps", DISPATCH_QUEUE_SERIAL);
|
|
||||||
if (!gFamiliesQueue) gFamiliesQueue = dispatch_queue_create(
|
if (!gFamiliesQueue) gFamiliesQueue = dispatch_queue_create(
|
||||||
"com.filzaslop.worsequery.families", DISPATCH_QUEUE_SERIAL);
|
"com.filzaslop.worsequery.families", DISPATCH_QUEUE_SERIAL);
|
||||||
|
|
||||||
// App containers are the priority: the Apps manager must populate before
|
// App containers are the priority: the Apps manager must populate before
|
||||||
// the slower system-group sweep begins.
|
// the slower system-group sweep begins.
|
||||||
dispatch_group_enter(gStartupGroup);
|
dispatch_group_enter(gStartupGroup);
|
||||||
dispatch_async(gAppsQueue, ^{
|
dispatch_async(WQAppsQueue(), ^{
|
||||||
WQPostStartupProgress(0.24, @"正在枚举应用数据容器…");
|
WQPostStartupProgress(0.24, @"正在枚举应用数据容器…");
|
||||||
if (!gAppsCache) gAppsCache = WQBuildAppList();
|
if (!gAppsCache) gAppsCache = WQBuildAppList();
|
||||||
gAppsCacheDate = [NSDate date];
|
gAppsCacheDate = [NSDate date];
|
||||||
@@ -1587,6 +1766,18 @@ void WQFilzaStart(void)
|
|||||||
- (NSNumber *)dynamicDiskUsage { return @0; }
|
- (NSNumber *)dynamicDiskUsage { return @0; }
|
||||||
- (BOOL)isDeletable { return YES; }
|
- (BOOL)isDeletable { return YES; }
|
||||||
- (BOOL)isPlaceholder { return NO; }
|
- (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
|
@end
|
||||||
Reference in new issue
Block a user