Compare commits

...

17 Commits

Author SHA1 Message Date
Alfie CG 783ab43c3e
Merge pull request #635 from dhinakg/main
Arm developer mode if needed
2023-12-30 22:39:41 +00:00
Dhinak G c1090cf790
Change close buttons to `UIAlertActionStyleCancel` 2023-12-30 14:15:26 -05:00
Dhinak G 9f9fd76310
Add FrontBoardServices to Makefiles 2023-12-30 13:40:05 -05:00
Dhinak G fa948c0646
Fix last 2023-12-30 13:38:09 -05:00
Dhinak G e157415304
Merge remote-tracking branch 'upstream/main' 2023-12-30 13:35:00 -05:00
Dhinak G 3474468189
Fix dev mode specifier always showing up 2023-12-30 13:32:02 -05:00
Dhinak G eed1d42792
Fix rebooting
reboot3 requires platformization. Use FrontBoardServices to do it instead
2023-12-30 13:23:54 -05:00
Dhinak G 28aab08dec
Fix last 2023-12-22 00:27:09 -05:00
Dhinak G 8dc50d7555
Add reboot code 2023-12-22 00:08:23 -05:00
Dhinak G f1f42778d8
Invert output from RootHelper 2023-12-21 23:21:26 -05:00
Dhinak G d502576e1f
Remove dev mode from app info 2023-12-21 21:39:31 -05:00
Dhinak G afb45b110e
Fix issue in entitlement checking 2023-12-21 21:38:47 -05:00
Dhinak G a56bf738bd
Add in other restricted entitlements 2023-12-02 23:45:41 -05:00
Dhinak G 5eecb677a7
Document the actions 2023-11-30 20:34:56 -05:00
Dhinak G c130a04ff5
Fix copy & paste typo 2023-11-30 20:19:06 -05:00
Dhinak G f57326e0a4
Show developer mode status in app info 2023-11-30 19:52:00 -05:00
Dhinak G 2ac6bc280f
Add code to check and arm developer mode 2023-11-30 19:51:51 -05:00
10 changed files with 335 additions and 29 deletions

View File

@ -14,6 +14,6 @@ trollstorehelper_CODESIGN_FLAGS = --entitlements entitlements.plist
trollstorehelper_INSTALL_PATH = /usr/local/bin trollstorehelper_INSTALL_PATH = /usr/local/bin
trollstorehelper_LIBRARIES = archive trollstorehelper_LIBRARIES = archive
trollstorehelper_FRAMEWORKS = CoreTelephony trollstorehelper_FRAMEWORKS = CoreTelephony
trollstorehelper_PRIVATE_FRAMEWORKS = SpringBoardServices BackBoardServices MobileContainerManager trollstorehelper_PRIVATE_FRAMEWORKS = SpringBoardServices BackBoardServices MobileContainerManager FrontBoardServices
include $(THEOS_MAKE_PATH)/tool.mk include $(THEOS_MAKE_PATH)/tool.mk

4
RootHelper/devmode.h Normal file
View File

@ -0,0 +1,4 @@
#import <Foundation/Foundation.h>
BOOL checkDeveloperMode(void);
BOOL armDeveloperMode(BOOL* alreadyEnabled);

142
RootHelper/devmode.m Normal file
View File

@ -0,0 +1,142 @@
@import Foundation;
// Types
typedef NSObject* xpc_object_t;
typedef xpc_object_t xpc_connection_t;
typedef void (^xpc_handler_t)(xpc_object_t object);
// Serialization
extern CFTypeRef _CFXPCCreateCFObjectFromXPCObject(xpc_object_t xpcattrs);
extern xpc_object_t _CFXPCCreateXPCObjectFromCFObject(CFTypeRef attrs);
extern xpc_object_t _CFXPCCreateXPCMessageWithCFObject(CFTypeRef obj);
extern CFTypeRef _CFXPCCreateCFObjectFromXPCMessage(xpc_object_t obj);
// Communication
extern xpc_connection_t xpc_connection_create_mach_service(const char* name, dispatch_queue_t targetq, uint64_t flags);
extern void xpc_connection_set_event_handler(xpc_connection_t connection, xpc_handler_t handler);
extern void xpc_connection_resume(xpc_connection_t connection);
extern void xpc_connection_send_message_with_reply(xpc_connection_t connection, xpc_object_t message, dispatch_queue_t replyq, xpc_handler_t handler);
extern xpc_object_t xpc_connection_send_message_with_reply_sync(xpc_connection_t connection, xpc_object_t message);
extern xpc_object_t xpc_dictionary_get_value(xpc_object_t xdict, const char *key);
typedef enum {
kAMFIActionArm = 0, // Trigger a prompt asking the user to enable developer mode on the next reboot
// (regardless of current state)
kAMFIActionDisable = 1, // Disable developer mode if it's currently enabled. Takes effect immediately.
kAMFIActionStatus = 2, // Returns a dict: {success: bool, status: bool, armed: bool}
} AMFIXPCAction;
xpc_connection_t startConnection(void) {
xpc_connection_t connection = xpc_connection_create_mach_service("com.apple.amfi.xpc", NULL, 0);
if (!connection) {
NSLog(@"[startXPCConnection] Failed to create XPC connection to amfid");
return nil;
}
xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
});
xpc_connection_resume(connection);
return connection;
}
NSDictionary* sendXPCRequest(xpc_connection_t connection, AMFIXPCAction action) {
xpc_object_t message = _CFXPCCreateXPCMessageWithCFObject((__bridge CFDictionaryRef) @{@"action": @(action)});
xpc_object_t replyMsg = xpc_connection_send_message_with_reply_sync(connection, message);
if (!replyMsg) {
NSLog(@"[sendXPCRequest] got no reply from amfid");
return nil;
}
xpc_object_t replyObj = xpc_dictionary_get_value(replyMsg, "cfreply");
if (!replyObj) {
NSLog(@"[sendXPCRequest] got reply but no cfreply");
return nil;
}
NSDictionary* asCF = (__bridge NSDictionary*)_CFXPCCreateCFObjectFromXPCMessage(replyObj);
return asCF;
}
BOOL getDeveloperModeState(xpc_connection_t connection) {
NSDictionary* reply = sendXPCRequest(connection, kAMFIActionStatus);
if (!reply) {
NSLog(@"[getDeveloperModeState] failed to get reply");
return NO;
}
NSLog(@"[getDeveloperModeState] got reply %@", reply);
NSObject* success = reply[@"success"];
if (!success || ![success isKindOfClass:[NSNumber class]] || ![(NSNumber*)success boolValue]) {
NSLog(@"[getDeveloperModeState] request failed with error %@", reply[@"error"]);
return NO;
}
NSObject* status = reply[@"status"];
if (!status || ![status isKindOfClass:[NSNumber class]]) {
NSLog(@"[getDeveloperModeState] request succeeded but no status");
return NO;
}
return [(NSNumber*)status boolValue];
}
BOOL setDeveloperModeState(xpc_connection_t connection, BOOL enable) {
NSDictionary* reply = sendXPCRequest(connection, enable ? kAMFIActionArm : kAMFIActionDisable);
if (!reply) {
NSLog(@"[setDeveloperModeState] failed to get reply");
return NO;
}
NSObject* success = reply[@"success"];
if (!success || ![success isKindOfClass:[NSNumber class]] || ![(NSNumber*)success boolValue]) {
NSLog(@"[setDeveloperModeState] request failed with error %@", reply[@"error"]);
return NO;
}
return YES;
}
BOOL checkDeveloperMode(void) {
// Developer mode does not exist before iOS 16
if (@available(iOS 16, *)) {
xpc_connection_t connection = startConnection();
if (!connection) {
NSLog(@"[checkDeveloperMode] failed to start connection");
// Assume it's disabled
return NO;
}
return getDeveloperModeState(connection);
} else {
return YES;
}
}
BOOL armDeveloperMode(BOOL* alreadyEnabled) {
// Developer mode does not exist before iOS 16
if (@available(iOS 16, *)) {
xpc_connection_t connection = startConnection();
if (!connection) {
NSLog(@"[armDeveloperMode] failed to start connection");
return NO;
}
BOOL enabled = getDeveloperModeState(connection);
if (alreadyEnabled) {
*alreadyEnabled = enabled;
}
if (enabled) {
// NSLog(@"[armDeveloperMode] already enabled");
return YES;
}
BOOL success = setDeveloperModeState(connection, YES);
if (!success) {
NSLog(@"[armDeveloperMode] failed to arm");
return NO;
}
}
return YES;
}

View File

@ -44,5 +44,9 @@
<string>Uninstall</string> <string>Uninstall</string>
<string>UpdatePlaceholderMetadata</string> <string>UpdatePlaceholderMetadata</string>
</array> </array>
<key>com.apple.private.amfi.developer-mode-control</key>
<true/>
<key>com.apple.frontboard.shutdown</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@ -10,6 +10,7 @@
#import <sys/utsname.h> #import <sys/utsname.h>
#import <mach-o/loader.h> #import <mach-o/loader.h>
#import <mach-o/fat.h> #import <mach-o/fat.h>
#import "devmode.h"
#ifndef EMBEDDED_ROOT_HELPER #ifndef EMBEDDED_ROOT_HELPER
#import "codesign.h" #import "codesign.h"
#import "coretrust_bug.h" #import "coretrust_bug.h"
@ -20,6 +21,7 @@
#endif #endif
#import <SpringBoardServices/SpringBoardServices.h> #import <SpringBoardServices/SpringBoardServices.h>
#import <FrontBoardServices/FBSSystemService.h>
#import <Security/Security.h> #import <Security/Security.h>
#ifdef EMBEDDED_ROOT_HELPER #ifdef EMBEDDED_ROOT_HELPER
@ -564,6 +566,10 @@ int signApp(NSString* appPath)
} }
} }
// On iOS 16+, binaries with certain entitlements requires developer mode to be enabled, so we'll check
// while we're fixing entitlements
BOOL requiresDevMode = NO;
NSURL* fileURL; NSURL* fileURL;
NSDirectoryEnumerator *enumerator; NSDirectoryEnumerator *enumerator;
@ -608,6 +614,25 @@ int signApp(NSString* appPath)
if (!entitlementsToUse) entitlementsToUse = [NSMutableDictionary new]; if (!entitlementsToUse) entitlementsToUse = [NSMutableDictionary new];
// Developer mode does not exist before iOS 16
if (@available(iOS 16, *)){
if (!requiresDevMode) {
for (NSString* restrictedEntitlementKey in @[
@"get-task-allow",
@"task_for_pid-allow",
@"com.apple.system-task-ports",
@"com.apple.system-task-ports.control",
@"com.apple.system-task-ports.token.control",
@"com.apple.private.cs.debugger"
]) {
NSObject *restrictedEntitlement = entitlementsToUse[restrictedEntitlementKey];
if (restrictedEntitlement && [restrictedEntitlement isKindOfClass:[NSNumber class]] && [(NSNumber *)restrictedEntitlement boolValue]) {
requiresDevMode = YES;
}
}
}
}
NSObject *containerRequiredO = entitlementsToUse[@"com.apple.private.security.container-required"]; NSObject *containerRequiredO = entitlementsToUse[@"com.apple.private.security.container-required"];
BOOL containerRequired = YES; BOOL containerRequired = YES;
if (containerRequiredO && [containerRequiredO isKindOfClass:[NSNumber class]]) { if (containerRequiredO && [containerRequiredO isKindOfClass:[NSNumber class]]) {
@ -686,6 +711,11 @@ int signApp(NSString* appPath)
} }
} }
if (requiresDevMode) {
// Postpone trying to enable dev mode until after the app is (successfully) installed
return 182;
}
return 0; return 0;
} }
#endif #endif
@ -770,10 +800,19 @@ int installApp(NSString* appPackagePath, BOOL sign, BOOL force, BOOL isTSUpdate,
applyPatchesToInfoDictionary(appBundleToInstallPath); applyPatchesToInfoDictionary(appBundleToInstallPath);
} }
BOOL requiresDevMode = NO;
if(sign) if(sign)
{ {
int signRet = signApp(appBundleToInstallPath); int signRet = signApp(appBundleToInstallPath);
if(signRet != 0) return signRet; // 182: app requires developer mode; non-fatal
if(signRet != 0) {
if (signRet == 182) {
requiresDevMode = YES;
} else {
return signRet;
}
};
} }
MCMAppContainer* appContainer = [MCMAppContainer containerWithIdentifier:appId createIfNecessary:NO existed:nil error:nil]; MCMAppContainer* appContainer = [MCMAppContainer containerWithIdentifier:appId createIfNecessary:NO existed:nil error:nil];
@ -919,6 +958,23 @@ int installApp(NSString* appPackagePath, BOOL sign, BOOL force, BOOL isTSUpdate,
[[NSFileManager defaultManager] removeItemAtURL:appContainer.url error:nil]; [[NSFileManager defaultManager] removeItemAtURL:appContainer.url error:nil];
return 181; return 181;
} }
// Handle developer mode after installing and registering the app, to ensure that we
// don't arm developer mode but then fail to install the app
if (requiresDevMode) {
BOOL alreadyEnabled = NO;
if (armDeveloperMode(&alreadyEnabled)) {
if (!alreadyEnabled) {
NSLog(@"[installApp] app requires developer mode and we have successfully armed it");
// non-fatal
return 182;
}
} else {
NSLog(@"[installApp] failed to arm developer mode");
// fatal
return 183;
}
}
return 0; return 0;
} }
@ -1470,6 +1526,22 @@ int MAIN_NAME(int argc, char *argv[], char *envp[])
setTSURLSchemeState(newState, nil); setTSURLSchemeState(newState, nil);
} }
} }
else if([cmd isEqualToString:@"check-dev-mode"])
{
// switch the result, so 0 is enabled, and 1 is disabled/error
ret = !checkDeveloperMode();
}
else if([cmd isEqualToString:@"arm-dev-mode"])
{
// assumes that checkDeveloperMode() has already been called
ret = !armDeveloperMode(NULL);
}
else if([cmd isEqualToString:@"reboot"])
{
[[FBSSystemService sharedService] reboot];
// Give the system some time to reboot
sleep(1);
}
NSLog(@"trollstorehelper returning %d", ret); NSLog(@"trollstorehelper returning %d", ret);
return ret; return ret;

View File

@ -34,7 +34,7 @@ ifeq ($(EMBEDDED_ROOT_HELPER),1)
TrollStorePersistenceHelper_CFLAGS += -DEMBEDDED_ROOT_HELPER=1 TrollStorePersistenceHelper_CFLAGS += -DEMBEDDED_ROOT_HELPER=1
TrollStorePersistenceHelper_FILES += $(wildcard ../RootHelper/*.m) TrollStorePersistenceHelper_FILES += $(wildcard ../RootHelper/*.m)
TrollStorePersistenceHelper_LIBRARIES += archive TrollStorePersistenceHelper_LIBRARIES += archive
TrollStorePersistenceHelper_PRIVATE_FRAMEWORKS += SpringBoardServices BackBoardServices TrollStorePersistenceHelper_PRIVATE_FRAMEWORKS += SpringBoardServices BackBoardServices FrontBoardServices
endif endif
include $(THEOS_MAKE_PATH)/application.mk include $(THEOS_MAKE_PATH)/application.mk

View File

@ -80,6 +80,12 @@ extern NSUserDefaults* trollStoreUserDefaults();
case 181: case 181:
errorDescription = @"Failed to add app to icon cache."; errorDescription = @"Failed to add app to icon cache.";
break; break;
case 182:
errorDescription = @"The app was installed successfully, but requires developer mode to be enabled to run. After rebooting, select \"Turn On\" to enable developer mode.";
break;
case 183:
errorDescription = @"Failed to enable developer mode.";
break;
} }
NSError* error = [NSError errorWithDomain:TrollStoreErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : errorDescription}]; NSError* error = [NSError errorWithDomain:TrollStoreErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : errorDescription}];

View File

@ -32,42 +32,58 @@ extern NSUserDefaults* trollStoreUserDefaults(void);
{ {
[TSPresentationDelegate stopActivityWithCompletion:^ [TSPresentationDelegate stopActivityWithCompletion:^
{ {
if(ret != 0) if (ret == 0) {
{ // success
if(completionBlock) completionBlock(YES, nil);
} else if (ret == 171) {
// recoverable error
UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Install Error %d", ret] message:[error localizedDescription] preferredStyle:UIAlertControllerStyleAlert]; UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Install Error %d", ret] message:[error localizedDescription] preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action)
{
if(ret == 171)
{ {
if(completionBlock) completionBlock(NO, error); if(completionBlock) completionBlock(NO, error);
}
}]; }];
[errorAlert addAction:closeAction]; [errorAlert addAction:closeAction];
if(ret == 171)
{
UIAlertAction* forceInstallAction = [UIAlertAction actionWithTitle:@"Force Installation" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) UIAlertAction* forceInstallAction = [UIAlertAction actionWithTitle:@"Force Installation" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action)
{ {
[self handleAppInstallFromFile:pathToIPA forceInstall:YES completion:completionBlock]; [self handleAppInstallFromFile:pathToIPA forceInstall:YES completion:completionBlock];
}]; }];
[errorAlert addAction:forceInstallAction]; [errorAlert addAction:forceInstallAction];
}
else [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil];
} else if (ret == 182) {
// non-fatal informative message
UIAlertController* rebootNotification = [UIAlertController alertControllerWithTitle:@"Reboot Required" message:[error localizedDescription] preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleCancel handler:^(UIAlertAction* action)
{ {
if(completionBlock) completionBlock(YES, nil);
}];
[rebootNotification addAction:closeAction];
UIAlertAction* rebootAction = [UIAlertAction actionWithTitle:@"Reboot Now" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action)
{
if(completionBlock) completionBlock(YES, nil);
spawnRoot(rootHelperPath(), @[@"reboot"], nil, nil);
}];
[rebootNotification addAction:rebootAction];
[TSPresentationDelegate presentViewController:rebootNotification animated:YES completion:nil];
} else {
// unrecoverable error
UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Install Error %d", ret] message:[error localizedDescription] preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil];
[errorAlert addAction:closeAction];
UIAlertAction* copyLogAction = [UIAlertAction actionWithTitle:@"Copy Debug Log" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) UIAlertAction* copyLogAction = [UIAlertAction actionWithTitle:@"Copy Debug Log" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action)
{ {
UIPasteboard* pasteboard = [UIPasteboard generalPasteboard]; UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = log; pasteboard.string = log;
}]; }];
[errorAlert addAction:copyLogAction]; [errorAlert addAction:copyLogAction];
}
[TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil]; [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil];
}
if(ret != 171) if(completionBlock) completionBlock(NO, error);
{
if(completionBlock) completionBlock((BOOL)error, error);
} }
}]; }];
}); });

View File

@ -5,5 +5,6 @@
PSSpecifier* _installPersistenceHelperSpecifier; PSSpecifier* _installPersistenceHelperSpecifier;
NSString* _newerVersion; NSString* _newerVersion;
NSString* _newerLdidVersion; NSString* _newerLdidVersion;
BOOL _devModeEnabled;
} }
@end @end

View File

@ -55,6 +55,16 @@ extern NSUserDefaults* trollStoreUserDefaults(void);
} }
}); });
//} //}
if (@available(iOS 16, *))
{
_devModeEnabled = spawnRoot(rootHelperPath(), @[@"check-dev-mode"], nil, nil) == 0;
}
else
{
_devModeEnabled = YES;
}
[self reloadSpecifiers];
} }
- (NSMutableArray*)specifiers - (NSMutableArray*)specifiers
@ -82,6 +92,26 @@ extern NSUserDefaults* trollStoreUserDefaults(void);
[_specifiers addObject:updateTrollStoreSpecifier]; [_specifiers addObject:updateTrollStoreSpecifier];
} }
if(!_devModeEnabled)
{
PSSpecifier* enableDevModeGroupSpecifier = [PSSpecifier emptyGroupSpecifier];
enableDevModeGroupSpecifier.name = @"Developer Mode";
[enableDevModeGroupSpecifier setProperty:@"Some apps require developer mode enabled to launch. This requires a reboot to take effect." forKey:@"footerText"];
[_specifiers addObject:enableDevModeGroupSpecifier];
PSSpecifier* enableDevModeSpecifier = [PSSpecifier preferenceSpecifierNamed:@"Enable Developer Mode"
target:self
set:nil
get:nil
detail:nil
cell:PSButtonCell
edit:nil];
enableDevModeSpecifier.identifier = @"enableDevMode";
[enableDevModeSpecifier setProperty:@YES forKey:@"enabled"];
enableDevModeSpecifier.buttonAction = @selector(enableDevModePressed);
[_specifiers addObject:enableDevModeSpecifier];
}
PSSpecifier* utilitiesGroupSpecifier = [PSSpecifier emptyGroupSpecifier]; PSSpecifier* utilitiesGroupSpecifier = [PSSpecifier emptyGroupSpecifier];
utilitiesGroupSpecifier.name = @"Utilities"; utilitiesGroupSpecifier.name = @"Utilities";
[utilitiesGroupSpecifier setProperty:@"If an app does not immediately appear after installation, respring here and it should appear afterwards." forKey:@"footerText"]; [utilitiesGroupSpecifier setProperty:@"If an app does not immediately appear after installation, respring here and it should appear afterwards." forKey:@"footerText"];
@ -369,6 +399,37 @@ extern NSUserDefaults* trollStoreUserDefaults(void);
[TSInstallationController installLdid]; [TSInstallationController installLdid];
} }
- (void)enableDevModePressed
{
int ret = spawnRoot(rootHelperPath(), @[@"arm-dev-mode"], nil, nil);
if (ret == 0) {
UIAlertController* rebootNotification = [UIAlertController alertControllerWithTitle:@"Reboot Required"
message:@"After rebooting, select \"Turn On\" to enable developer mode."
preferredStyle:UIAlertControllerStyleAlert
];
UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleCancel handler:^(UIAlertAction* action)
{
[self reloadSpecifiers];
}];
[rebootNotification addAction:closeAction];
UIAlertAction* rebootAction = [UIAlertAction actionWithTitle:@"Reboot Now" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action)
{
spawnRoot(rootHelperPath(), @[@"reboot"], nil, nil);
}];
[rebootNotification addAction:rebootAction];
[TSPresentationDelegate presentViewController:rebootNotification animated:YES completion:nil];
} else {
UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Error %d", ret] message:@"Failed to enable developer mode." preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil];
[errorAlert addAction:closeAction];
[TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil];
}
}
- (void)installPersistenceHelperPressed - (void)installPersistenceHelperPressed
{ {
NSMutableArray* appCandidates = [NSMutableArray new]; NSMutableArray* appCandidates = [NSMutableArray new];