2 // CircleJoinRequested.m
5 // Created by J Osborne on 3/5/13.
6 // Copyright (c) 2013-2014 Apple Inc. All Rights Reserved.
8 #import <Accounts/Accounts.h>
9 #import <Accounts/ACAccountStore_Private.h>
10 #pragma clang diagnostic push
11 #pragma clang diagnostic ignored "-Wnewline-eof"
12 #import <AppleAccount/AppleAccount.h>
13 #pragma clang diagnostic pop
14 #import <AppleAccount/ACAccountStore+AppleAccount.h>
15 #import <Accounts/ACAccountType_Private.h>
16 #import <Foundation/Foundation.h>
17 #include <dispatch/dispatch.h>
18 #include "SecureObjectSync/SOSCloudCircle.h"
19 #include "SecureObjectSync/SOSPeerInfo.h"
20 #import <CoreFoundation/CFUserNotification.h>
21 #import <SpringBoardServices/SBSCFUserNotificationKeys.h>
25 #import "NSArray+map.h"
26 #import <ManagedConfiguration/MCProfileConnection.h>
27 #import <ManagedConfiguration/MCFeatures.h>
28 #import <Security/SecFrameworkStrings.h>
29 #import "PersistantState.h"
30 #include <xpc/private.h>
32 #import "NSDate+TimeIntervalDescription.h"
33 #include <MobileGestalt.h>
34 #include <xpc/activity.h>
35 #include <xpc/private.h>
36 #import <MobileCoreServices/MobileCoreServices.h>
37 #import <MobileCoreServices/LSApplicationWorkspace.h>
38 #import <CloudServices/SecureBackup.h>
41 // As long as we are logging the failure use exit code of zero to make launchd happy
42 #define EXIT_LOGGED_FAILURE(code) xpc_transaction_end(); exit(0)
44 const char *kLaunchLaterXPCName = "com.apple.security.CircleJoinRequestedTick";
45 CFRunLoopSourceRef currentAlertSource = NULL;
46 CFUserNotificationRef currentAlert = NULL;
47 bool currentAlertIsForApplicants = true;
48 bool currentAlertIsForKickOut = false;
49 NSMutableDictionary *applicants = nil;
50 volatile NSString *debugState = @"main?";
51 dispatch_block_t doOnceInMainBlockChain = NULL;
53 NSString *castleKeychainUrl = @"prefs:root=CASTLE&path=Keychain/ADVANCED";
56 // For use with: __attribute__((cleanup(CFReleaseSafeIndirect))) CFType auto_var;
57 static void CFReleaseSafeIndirect(void *o_)
66 static void doOnceInMain(dispatch_block_t block)
68 if (doOnceInMainBlockChain) {
69 doOnceInMainBlockChain = ^{
70 doOnceInMainBlockChain();
74 doOnceInMainBlockChain = block;
78 static NSString *appleIDAccountName()
80 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
81 ACAccount *primaryAppleAccount = [accountStore aa_primaryAppleAccount];
82 return primaryAppleAccount.username;
85 static CFOptionFlags flagsForAsk(Applicant *applicant)
87 return kCFUserNotificationPlainAlertLevel|CFUserNotificationSecureTextField(0);
90 // NOTE: gives precedence to OnScreen
91 static Applicant *firstApplicantWaitingOrOnScreen()
93 Applicant *waiting = nil;
94 for (Applicant *applicant in [applicants objectEnumerator]) {
95 if (applicant.applicantUIState == ApplicantOnScreen) {
97 } else if (applicant.applicantUIState == ApplicantWaiting) {
105 static NSMutableArray *applicantsInState(ApplicantUIState state)
107 NSMutableArray *results = [NSMutableArray new];
108 for (Applicant *applicant in [applicants objectEnumerator]) {
109 if (applicant.applicantUIState == state) {
110 [results addObject:applicant];
117 static BOOL processRequests(CFErrorRef *error)
120 NSMutableArray *toAccept = [[applicantsInState(ApplicantAccepted) mapWithBlock:^id(id obj) {
121 return (id)[obj rawPeerInfo];
123 NSMutableArray *toReject = [[applicantsInState(ApplicantRejected) mapWithBlock:^id(id obj) {
124 return (id)[obj rawPeerInfo];
127 NSLog(@"Process accept: %@", toAccept);
128 NSLog(@"Process reject: %@", toReject);
130 if ([toAccept count]) {
131 ok = ok && SOSCCAcceptApplicants((__bridge CFArrayRef)(toAccept), error);
133 if ([toReject count]) {
134 ok = ok && SOSCCRejectApplicants((__bridge CFArrayRef)(toReject), error);
140 static void cancelCurrentAlert(bool stopRunLoop)
142 if (currentAlertSource) {
143 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
144 CFRelease(currentAlertSource);
145 currentAlertSource = NULL;
148 CFUserNotificationCancel(currentAlert);
149 CFRelease(currentAlert);
153 CFRunLoopStop(CFRunLoopGetCurrent());
155 currentAlertIsForKickOut = currentAlertIsForApplicants = false;
158 static void askAboutAll(bool passwordFailure);
160 static void applicantChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
162 ApplicantUIState choice;
164 if (kCFUserNotificationAlternateResponse == responseFlags) {
165 choice = ApplicantRejected;
166 } else if (kCFUserNotificationDefaultResponse == responseFlags) {
167 choice = ApplicantAccepted;
169 NSLog(@"Unexpected response %lu", responseFlags);
170 choice = ApplicantRejected;
174 CFErrorRef error = NULL;
176 NSArray *onScreen = applicantsInState(ApplicantOnScreen);
178 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
179 Applicant* applicant = (Applicant*) obj;
181 applicant.applicantUIState = choice;
184 if (choice == ApplicantRejected) {
185 // If this device has ever set up the public key this should work without the password...
186 processed = processRequests(&error);
188 NSLog(@"Didn't need password to process %@", onScreen);
189 cancelCurrentAlert(true);
192 // ...however if the public key gets lost we should "just" fall through to the validate
194 NSLog(@"Couldn't process reject without password (e=%@) for %@ (will try with password next)", error, onScreen);
198 NSString *password = (__bridge NSString *)(CFUserNotificationGetResponseValue(userNotification, kCFUserNotificationTextFieldValuesKey, 0));
200 NSLog(@"No password given, retry");
204 const char *passwordUTF8 = [password UTF8String];
205 NSData *passwordBytes = [NSData dataWithBytes:passwordUTF8 length:strlen(passwordUTF8)];
207 // Sometimes securityd crashes between the SOSCCRegisterUserCredentials and the processRequests,
208 // (which results in a process error -- I think this is 13355140); as a workaround we retry
209 // failure a few times before we give up.
210 for (int try = 0; try < 5 && !processed; try++) {
211 if (!SOSCCTryUserCredentials(CFSTR(""), (__bridge CFDataRef)(passwordBytes), &error)) {
212 NSLog(@"Try user credentials failed %@", error);
213 if ((error==NULL) || (CFEqual(kSOSErrorDomain, CFErrorGetDomain(error)) && kSOSErrorWrongPassword == CFErrorGetCode(error))) {
214 NSLog(@"Calling askAboutAll again...");
216 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
217 Applicant* applicant = (Applicant*) obj;
219 applicant.applicantUIState = ApplicantWaiting;
224 EXIT_LOGGED_FAILURE(EX_DATAERR);
227 processed = processRequests(&error);
229 NSLog(@"Can't processRequests: %@ for %@", error, onScreen);
232 if (processed && firstApplicantWaitingOrOnScreen()) {
233 cancelCurrentAlert(false);
236 cancelCurrentAlert(true);
240 static void passwordFailurePrompt()
243 NSString *pwIncorrect = [NSString stringWithFormat:(NSString *)CFBridgingRelease(SecCopyCKString(SEC_CK_PASSWORD_INCORRECT)), appleIDAccountName()];
244 NSString *tryAgain = CFBridgingRelease(SecCopyCKString(SEC_CK_TRY_AGAIN));
245 NSDictionary *noteAttributes = @{
246 (id)kCFUserNotificationAlertHeaderKey: pwIncorrect,
247 (id)kCFUserNotificationDefaultButtonTitleKey: tryAgain,
248 // TopMost gets us onto the lock screen
249 (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
250 (__bridge id)SBUserNotificationDontDismissOnUnlock: @YES,
251 (__bridge id)SBUserNotificationDismissOnLock: @NO,
253 CFOptionFlags flags = kCFUserNotificationPlainAlertLevel;
255 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef)noteAttributes);
256 CFUserNotificationReceiveResponse(note, 0.0, &flags);
260 static NSDictionary *createNote(Applicant *applicantToAskAbout) {
261 if(!applicantToAskAbout) return NULL;
262 NSString *appName = applicantToAskAbout.name;
263 if(!appName) return NULL;
264 NSString *devType = applicantToAskAbout.deviceType;
265 if(!devType) return NULL;
267 (id)kCFUserNotificationAlertHeaderKey: [NSString stringWithFormat:(__bridge_transfer NSString*)SecCopyCKString(SEC_CK_JOIN_TITLE), appName],
268 (id)kCFUserNotificationAlertMessageKey: [NSString stringWithFormat:(__bridge_transfer NSString*)SecCopyCKString(SEC_CK_JOIN_PROMPT), appleIDAccountName(), devType],
269 (id)kCFUserNotificationDefaultButtonTitleKey: (__bridge_transfer NSString*)SecCopyCKString(SEC_CK_ALLOW),
270 (id)kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString*)SecCopyCKString(SEC_CK_DONT_ALLOW),
271 (id)kCFUserNotificationTextFieldTitlesKey: (__bridge_transfer NSString*)SecCopyCKString(SEC_CK_ICLOUD_PASSWORD),
272 // TopMost gets us onto the lock screen
273 (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
274 (__bridge_transfer id)SBUserNotificationDontDismissOnUnlock: @YES,
275 (__bridge_transfer id)SBUserNotificationDismissOnLock: @NO,
279 static void askAboutAll(bool passwordFailure)
281 if ([[MCProfileConnection sharedConnection] effectiveBoolValueForSetting: MCFeatureAccountModificationAllowed] == MCRestrictedBoolExplicitNo) {
282 NSLog(@"Account modifications not allowed.");
286 if (passwordFailure) {
287 passwordFailurePrompt();
290 if ((passwordFailure || !currentAlertIsForApplicants) && currentAlert) {
291 if (!currentAlertIsForApplicants) {
292 CFUserNotificationCancel(currentAlert);
294 // after password failure we need to remove the existing alert and supporting objects
295 // because we can't reuse them.
296 CFRelease(currentAlert);
298 if (currentAlertSource) {
299 CFRelease(currentAlertSource);
300 currentAlertSource = NULL;
303 currentAlertIsForApplicants = true;
305 Applicant *applicantToAskAbout = firstApplicantWaitingOrOnScreen();
306 NSLog(@"Asking about: %@ (of: %@)", applicantToAskAbout, applicants);
308 NSDictionary *noteAttributes = createNote(applicantToAskAbout);
309 if(!noteAttributes) {
310 NSLog(@"NULL data for %@", applicantToAskAbout);
311 cancelCurrentAlert(true);
315 CFOptionFlags flags = flagsForAsk(applicantToAskAbout);
318 SInt32 err = CFUserNotificationUpdate(currentAlert, 0, flags, (__bridge CFDictionaryRef)noteAttributes);
320 NSLog(@"CFUserNotificationUpdate err=%d", (int)err);
321 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
325 currentAlert = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef)(noteAttributes));
327 NSLog(@"Can't make notification for %@ err=%x", applicantToAskAbout, (int)err);
328 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
331 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, applicantChoice, 0);
332 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
335 applicantToAskAbout.applicantUIState = ApplicantOnScreen;
338 static void scheduleActivity(int alertInterval)
340 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
341 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
342 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
343 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
344 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
345 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
347 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
348 NSLog(@"activity handler fired");
352 static void reminderChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
354 if (kCFUserNotificationAlternateResponse == responseFlags || kCFUserNotificationDefaultResponse == responseFlags) {
355 PersistantState *state = [PersistantState loadFromStorage];
356 NSDate *nowish = [NSDate new];
357 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
358 scheduleActivity(state.pendingApplicationReminderAlertInterval);
359 [state writeToStorage];
360 if (kCFUserNotificationAlternateResponse == responseFlags) {
361 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
362 NSLog(@"ok=%d opening %@", ok, [NSURL URLWithString:castleKeychainUrl]);
366 cancelCurrentAlert(true);
370 static NSString* getLocalizedDeviceClass(void) {
371 NSString *deviceType = NULL;
372 switch (MGGetSInt32Answer(kMGQDeviceClassNumber, MGDeviceClassInvalid)) {
373 case MGDeviceClassiPhone:
374 deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_IPHONE);
376 case MGDeviceClassiPod:
377 deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_IPOD);
379 case MGDeviceClassiPad:
380 deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_IPAD);
383 deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_DEVICE);
389 static bool iCloudResetAvailable()
391 SecureBackup *backupd = [SecureBackup new];
392 NSDictionary *backupdResults;
393 NSError *error = [backupd getAccountInfoWithInfo:nil results:&backupdResults];
394 NSLog(@"SecureBackup e=%@ r=%@", error, backupdResults);
395 return (nil == error && [backupdResults[kSecureBackupIsEnabledKey] isEqualToNumber:@YES]);
398 static void postApplicationReminderAlert(NSDate *nowish, PersistantState *state, unsigned int alertInterval)
401 NSString *deviceType = getLocalizedDeviceClass();
403 bool has_iCSC = iCloudResetAvailable();
405 NSString *alertMessage = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(has_iCSC ? SEC_CK_ARS1_BODY : SEC_CK_ARS0_BODY), deviceType];
406 if (state.defaultPendingApplicationReminderAlertInterval != state.pendingApplicationReminderAlertInterval) {
407 alertMessage = [NSString stringWithFormat:@"%@ 〖debug interval %u; wait time %@〗",
409 state.pendingApplicationReminderAlertInterval,
410 [nowish copyDescriptionOfIntervalSince:state.applcationDate]];
413 NSDictionary *pendingAttributes = @{
414 (id)kCFUserNotificationAlertHeaderKey: (__bridge NSString*)SecCopyCKString(has_iCSC ? SEC_CK_ARS1_TITLE : SEC_CK_ARS0_TITLE),
415 (id)kCFUserNotificationAlertMessageKey: alertMessage,
416 (id)kCFUserNotificationDefaultButtonTitleKey: (__bridge NSString*)SecCopyCKString(SEC_CK_AR_APPROVE_OTHER),
417 (id)kCFUserNotificationAlternateButtonTitleKey: has_iCSC ? (__bridge NSString*)SecCopyCKString(SEC_CK_AR_USE_CODE) : @"",
418 (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
419 (__bridge id)SBUserNotificationHideButtonsInAwayView: @YES,
420 (__bridge id)SBUserNotificationDontDismissOnUnlock: @YES,
421 (__bridge id)SBUserNotificationDismissOnLock: @NO,
422 (__bridge id)SBUserNotificationOneButtonPerLine: @YES,
425 currentAlert = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef)(pendingAttributes));
428 NSLog(@"Can't make pending notification err=%x", (int)err);
430 currentAlertIsForApplicants = false;
431 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, reminderChoice, 0);
432 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
436 static void kickOutChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
438 NSLog(@"kOC %@ %lu", userNotification, responseFlags);
439 if (kCFUserNotificationAlternateResponse == responseFlags) {
440 // We need to let things unwind to main for the new state to get saved
442 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
443 NSLog(@"ok=%d opening %@", ok, [NSURL URLWithString:castleKeychainUrl]);
446 cancelCurrentAlert(true);
449 static void postKickedOutAlert(enum DepartureReason reason)
451 NSString *deviceType = getLocalizedDeviceClass();
452 NSString *message = nil;
453 debugState = @"pKOA A";
454 bool ok_to_use_code = iCloudResetAvailable();
455 debugState = @"pKOA B";
458 case kSOSNeverLeftCircle:
459 // Was: SEC_CK_CR_BODY_NEVER_LEFT
463 case kSOSWithdrewMembership:
464 // Was: SEC_CK_CR_BODY_WITHDREW
465 // "... if you turn off a switch you have some idea why the light is off" - Murf
469 case kSOSMembershipRevoked:
470 message = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(SEC_CK_CR_BODY_REVOKED), deviceType];
473 case kSOSLeftUntrustedCircle:
474 message = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(SEC_CK_CR_BODY_LEFT_UNTRUSTED), deviceType];
475 ok_to_use_code = false;
478 case kSOSNeverAppliedToCircle:
479 // We didn't get kicked out, we were never here. This should only happen if we changed iCloud accounts
480 // (and we had sync on in the previous one, and never had it on in the new one). As this is explicit
481 // user action alot of thd "Light switch" argument (above) applies.
486 message = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(SEC_CK_CR_BODY_UNKNOWN), deviceType];
487 ok_to_use_code = false;
488 syslog(LOG_ERR, "Unknown DepartureReason %d", reason);
492 NSDictionary *kickedAttributes = @{
493 (id)kCFUserNotificationAlertHeaderKey: (__bridge NSString*)SecCopyCKString(SEC_CK_CR_TITLE),
494 (id)kCFUserNotificationAlertMessageKey: message,
495 (id)kCFUserNotificationDefaultButtonTitleKey: (__bridge NSString*)SecCopyCKString(SEC_CK_CR_OK),
496 (id)kCFUserNotificationAlternateButtonTitleKey: ok_to_use_code ? (__bridge NSString*)SecCopyCKString(SEC_CK_CR_USE_CODE)
498 (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
499 (__bridge id)SBUserNotificationHideButtonsInAwayView: @YES,
500 (__bridge id)SBUserNotificationDontDismissOnUnlock: @YES,
501 (__bridge id)SBUserNotificationDismissOnLock: @NO,
502 (__bridge id)SBUserNotificationOneButtonPerLine: @YES,
506 if (currentAlertIsForKickOut) {
507 debugState = @"pKOA C";
508 NSLog(@"Updating existing alert %@ with %@", currentAlert, kickedAttributes);
509 CFUserNotificationUpdate(currentAlert, 0, kCFUserNotificationPlainAlertLevel, (__bridge CFDictionaryRef)(kickedAttributes));
511 debugState = @"pKOA D";
513 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef)(kickedAttributes));
515 NSLog(@"Can't make kicked out notification err=%x", (int)err);
517 currentAlertIsForApplicants = false;
518 currentAlertIsForKickOut = true;
521 NSLog(@"New ko alert %@ a=%@", currentAlert, kickedAttributes);
522 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, kickOutChoice, 0);
523 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
524 int backupStateChangeToken;
525 notify_register_dispatch("com.apple.EscrowSecurityAlert.reset", &backupStateChangeToken, dispatch_get_main_queue(), ^(int token) {
526 if (currentAlert == note) {
527 NSLog(@"Backup state might have changed (dS=%@)", debugState);
528 postKickedOutAlert(reason);
530 NSLog(@"Backup state may have changed, but we don't care anymore (dS=%@)", debugState);
533 debugState = @"pKOA E";
535 debugState = @"pKOA F";
536 notify_cancel(backupStateChangeToken);
539 debugState = @"pKOA Z";
542 static bool processEvents()
544 debugState = @"processEvents A";
545 CFErrorRef error = NULL;
546 CFErrorRef departError = NULL;
547 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&error);
548 NSDate *nowish = [NSDate date];
549 PersistantState *state = [PersistantState loadFromStorage];
550 enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError);
551 NSLog(@"CircleStatus %d -> %d{%d} (s=%p)", state.lastCircleStatus, circleStatus, departureReason, state);
553 NSTimeInterval timeUntilApplicationAlert = [state.pendingApplicationReminder timeIntervalSinceDate:nowish];
555 NSLog(@"Time until pendingApplicationReminder (%@) %f", [state.pendingApplicationReminder debugDescription], timeUntilApplicationAlert);
557 if (circleStatus == kSOSCCRequestPending && timeUntilApplicationAlert <= 0) {
558 debugState = @"reminderAlert";
559 postApplicationReminderAlert(nowish, state, state.pendingApplicationReminderAlertInterval);
560 } else if (circleStatus == kSOSCCRequestPending) {
561 scheduleActivity(ceil(timeUntilApplicationAlert));
564 if (((circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent) && state.lastCircleStatus == kSOSCCInCircle) || state.debugShowLeftReason || (circleStatus == kSOSCCNotInCircle && state.lastCircleStatus == kSOSCCCircleAbsent && state.absentCircleWithNoReason)) {
565 debugState = @"processEvents B";
566 // Use to be in the circle, now we aren't. We ought to tell the user why.
568 if (state.debugShowLeftReason) {
569 NSLog(@"debugShowLeftReason is %@", state.debugShowLeftReason);
570 departureReason = [state.debugShowLeftReason intValue];
571 state.debugShowLeftReason = nil;
573 [state writeToStorage];
576 if (kSOSDepartureReasonError != departureReason) {
577 if (circleStatus == kSOSCCCircleAbsent && departureReason == kSOSNeverLeftCircle) {
578 // We don't yet know why the circle has vanished, remember our current ignorance
579 state.absentCircleWithNoReason = YES;
581 state.absentCircleWithNoReason = NO;
583 NSLog(@"Depature reason %d", departureReason);
584 postKickedOutAlert(departureReason);
585 NSLog(@"pKOA returned (cS %d lCS %d)", circleStatus, state.lastCircleStatus);
587 NSLog(@"Can't get last depature reason: %@", departError);
591 debugState = @"processEvents C";
593 if (circleStatus != state.lastCircleStatus) {
594 SOSCCStatus lastCircleStatus = state.lastCircleStatus;
595 state.lastCircleStatus = circleStatus;
597 if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
598 state.applcationDate = nowish;
599 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
600 scheduleActivity(state.pendingApplicationReminderAlertInterval);
602 if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
603 NSLog(@"Pending request completed");
604 state.applcationDate = [NSDate distantPast];
605 state.pendingApplicationReminder = [NSDate distantFuture];
608 [state writeToStorage];
611 if (circleStatus != kSOSCCInCircle) {
612 if (circleStatus == kSOSCCRequestPending && currentAlert) {
614 CFUserNotificationRef postedAlert = currentAlert;
616 debugState = @"processEvents D1";
617 notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ifyToken, dispatch_get_main_queue(), ^(int token) {
618 if (postedAlert != currentAlert) {
619 NSLog(@"-- CC after original alert gone (currentAlertIsForApplicants %d, pA %p, cA %p -- %@)", currentAlertIsForApplicants ? 1:0, postedAlert, currentAlert, currentAlert);
620 notify_cancel(token);
622 CFErrorRef localError = NULL;
623 SOSCCStatus newCircleStatus = SOSCCThisDeviceIsInCircle(&localError);
624 if (newCircleStatus != kSOSCCRequestPending) {
625 if (newCircleStatus == kSOSCCError)
626 NSLog(@"No longer pending (nCS=%d, alert=%@) error: %@", newCircleStatus, currentAlert, localError);
628 NSLog(@"No longer pending (nCS=%d, alert=%@)", newCircleStatus, currentAlert);
629 cancelCurrentAlert(true);
631 NSLog(@"Still pending...");
635 debugState = @"processEvents D2";
636 NSLog(@"NOTE: currentAlertIsForApplicants %d, token %d", currentAlertIsForApplicants ? 1:0, notifyToken);
640 debugState = @"processEvents D3";
641 NSLog(@"SOSCCThisDeviceIsInCircle status %d, not checking applicants", circleStatus);
645 debugState = @"processEvents E";
646 applicants = [NSMutableDictionary new];
647 for (id applicantInfo in (__bridge_transfer NSArray *)(SOSCCCopyApplicantPeerInfo(&error))) {
648 Applicant *applicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef)(applicantInfo)];
649 applicants[applicant.idString] = applicant;
652 int notify_token = -42;
653 debugState = @"processEvents F";
654 int notify_register_status = notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ify_token, dispatch_get_main_queue(), ^(int token) {
655 NSLog(@"Notified: %s", kSOSCCCircleChangedNotification);
656 CFErrorRef circleStatusError = NULL;
658 bool needsUpdate = false;
659 CFErrorRef copyPeerError = NULL;
660 NSMutableSet *newIds = [NSMutableSet new];
661 for (id applicantInfo in (__bridge_transfer NSArray *)(SOSCCCopyApplicantPeerInfo(©PeerError))) {
662 Applicant *newApplicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef)(applicantInfo)];
663 [newIds addObject:newApplicant.idString];
664 Applicant *existingApplicant = applicants[newApplicant.idString];
665 if (existingApplicant) {
666 switch (existingApplicant.applicantUIState) {
667 case ApplicantWaiting:
668 applicants[newApplicant.idString] = newApplicant;
671 case ApplicantOnScreen:
672 newApplicant.applicantUIState = ApplicantOnScreen;
673 applicants[newApplicant.idString] = newApplicant;
677 NSLog(@"Update to %@ >> %@ with pending order, should work out Ok though", existingApplicant, newApplicant);
682 applicants[newApplicant.idString] = newApplicant;
686 NSLog(@"Could not update peer info array: %@", copyPeerError);
690 NSMutableArray *idsToRemoveFromApplicants = [NSMutableArray new];
691 for (NSString *exisitngId in [applicants keyEnumerator]) {
692 if (![newIds containsObject:exisitngId]) {
693 [idsToRemoveFromApplicants addObject:exisitngId];
697 [applicants removeObjectsForKeys:idsToRemoveFromApplicants];
699 if (newIds.count == 0) {
700 NSLog(@"All applicants were handled elsewhere");
701 cancelCurrentAlert(true);
703 SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&circleStatusError);
704 if (kSOSCCInCircle != currentCircleStatus) {
705 NSLog(@"Left circle (%d), not handing remaining %lu applicants", currentCircleStatus, (unsigned long)newIds.count);
706 cancelCurrentAlert(true);
711 NSLog(@"needsUpdate false, not updating alert");
714 NSLog(@"ACC token %d, status %d", notify_token, notify_register_status);
715 debugState = @"processEvents F2";
717 if (applicants.count == 0) {
718 NSLog(@"No applicants");
720 debugState = @"processEvents F3";
722 debugState = @"processEvents F4";
724 debugState = @"processEvents F5";
729 debugState = @"processEvents F6";
730 notify_cancel(notify_token);
731 debugState = @"processEvents DONE";
736 int main (int argc, const char * argv[])
739 xpc_transaction_begin();
741 // NOTE: DISPATCH_QUEUE_PRIORITY_LOW will not actually manage to drain events
742 // in a lot of cases (like circleStatus != kSOSCCInCircle)
743 xpc_set_event_stream_handler("com.apple.notifyd.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t object) {
744 char *event_description = xpc_copy_description(object);
745 NSLog(@"notifyd event: %s\nAlert (%p) %s %s\ndebugState: %@", event_description, currentAlert, currentAlertIsForApplicants ? "for applicants" : "!applicants", currentAlertIsForKickOut ? "KO" : "!KO", debugState);
746 free(event_description);
749 xpc_activity_register(kLaunchLaterXPCName, XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
754 while (falseInARow < 2) {
755 if (processEvents()) {
760 cancelCurrentAlert(false);
761 if (doOnceInMainBlockChain) {
762 doOnceInMainBlockChain();
763 doOnceInMainBlockChain = NULL;
769 xpc_transaction_end();