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 #import <AggregateDictionary/ADClient.h>
11 #import <AppleAccount/AppleAccount.h>
12 #import <AppleAccount/ACAccountStore+AppleAccount.h>
13 #import <Accounts/ACAccountType_Private.h>
14 #import <Foundation/Foundation.h>
15 #include <dispatch/dispatch.h>
16 #include "SecureObjectSync/SOSCloudCircle.h"
17 #include "SecureObjectSync/SOSPeerInfo.h"
18 #import <CoreFoundation/CFUserNotification.h>
19 #import <SpringBoardServices/SBSCFUserNotificationKeys.h>
23 #import "NSArray+map.h"
24 #import <ManagedConfiguration/MCProfileConnection.h>
25 #import <ManagedConfiguration/MCFeatures.h>
26 #import <Security/SecFrameworkStrings.h>
27 #import "PersistentState.h"
28 #include <xpc/private.h>
30 #import "NSDate+TimeIntervalDescription.h"
31 #include <MobileGestalt.h>
32 #include <xpc/activity.h>
33 #include <xpc/private.h>
34 #import <MobileCoreServices/MobileCoreServices.h>
35 #import <MobileCoreServices/LSApplicationWorkspace.h>
36 #import <CloudServices/SecureBackup.h>
37 #import <AppSupport/AppSupportUtils.h>
39 #include "utilities/SecCFRelease.h"
40 #include "utilities/debugging.h"
42 // As long as we are logging the failure use exit code of zero to make launchd happy
43 #define EXIT_LOGGED_FAILURE(code) xpc_transaction_end(); exit(0)
45 const char *kLaunchLaterXPCName = "com.apple.security.CircleJoinRequestedTick";
46 CFRunLoopSourceRef currentAlertSource = NULL;
47 CFUserNotificationRef currentAlert = NULL;
48 bool currentAlertIsForApplicants = true;
49 bool currentAlertIsForKickOut = false;
50 NSMutableDictionary *applicants = nil;
51 volatile NSString *debugState = @"main?";
52 dispatch_block_t doOnceInMainBlockChain = NULL;
54 NSString *castleKeychainUrl = @"prefs:root=CASTLE&path=Keychain/ADVANCED";
56 static void doOnceInMain(dispatch_block_t block)
58 if (doOnceInMainBlockChain) {
59 doOnceInMainBlockChain = ^{
60 doOnceInMainBlockChain();
64 doOnceInMainBlockChain = block;
69 static NSString *appleIDAccountName()
71 ACAccountStore *accountStore = [[ACAccountStore alloc] init];
72 ACAccount *primaryAppleAccount = [accountStore aa_primaryAppleAccount];
73 return primaryAppleAccount.username;
77 static CFOptionFlags flagsForAsk(Applicant *applicant)
79 return kCFUserNotificationPlainAlertLevel | CFUserNotificationSecureTextField(0);
83 // NOTE: gives precedence to OnScreen
84 static Applicant *firstApplicantWaitingOrOnScreen()
86 Applicant *waiting = nil;
87 for (Applicant *applicant in [applicants objectEnumerator]) {
88 if (applicant.applicantUIState == ApplicantOnScreen) {
90 } else if (applicant.applicantUIState == ApplicantWaiting) {
99 static NSMutableArray *applicantsInState(ApplicantUIState state)
101 NSMutableArray *results = [NSMutableArray new];
102 for (Applicant *applicant in [applicants objectEnumerator]) {
103 if (applicant.applicantUIState == state) {
104 [results addObject:applicant];
112 static BOOL processRequests(CFErrorRef *error) {
113 NSMutableArray *toAccept = [[applicantsInState(ApplicantAccepted) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
114 NSMutableArray *toReject = [[applicantsInState(ApplicantRejected) mapWithBlock:^id(id obj) {return (id)[obj rawPeerInfo];}] mutableCopy];
117 NSLog(@"Process accept: %@", toAccept);
118 NSLog(@"Process reject: %@", toReject);
120 if ([toAccept count])
121 ok = ok && SOSCCAcceptApplicants((__bridge CFArrayRef) toAccept, error);
123 if ([toReject count])
124 ok = ok && SOSCCRejectApplicants((__bridge CFArrayRef) toReject, error);
130 static void cancelCurrentAlert(bool stopRunLoop) {
131 if (currentAlertSource) {
132 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
133 CFReleaseNull(currentAlertSource);
136 CFUserNotificationCancel(currentAlert);
137 CFReleaseNull(currentAlert);
140 CFRunLoopStop(CFRunLoopGetCurrent());
142 currentAlertIsForKickOut = currentAlertIsForApplicants = false;
146 static void askAboutAll(bool passwordFailure);
149 static void applicantChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
151 ApplicantUIState choice;
153 if (kCFUserNotificationAlternateResponse == responseFlags) {
154 choice = ApplicantRejected;
155 } else if (kCFUserNotificationDefaultResponse == responseFlags) {
156 choice = ApplicantAccepted;
158 NSLog(@"Unexpected response %lu", responseFlags);
159 choice = ApplicantRejected;
163 CFErrorRef error = NULL;
164 NSArray *onScreen = applicantsInState(ApplicantOnScreen);
166 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
167 Applicant* applicant = (Applicant *) obj;
168 applicant.applicantUIState = choice;
171 if (choice == ApplicantRejected) {
172 // If this device has ever set up the public key this should work without the password...
173 processed = processRequests(&error);
175 NSLog(@"Didn't need password to process %@", onScreen);
176 cancelCurrentAlert(true);
179 // ...however if the public key gets lost we should "just" fall through to the validate
181 NSLog(@"Couldn't process reject without password (e=%@) for %@ (will try with password next)", error, onScreen);
183 CFReleaseNull(error);
186 NSString *password = (__bridge NSString *)(CFUserNotificationGetResponseValue(userNotification, kCFUserNotificationTextFieldValuesKey, 0));
188 NSLog(@"No password given, retry");
192 const char *passwordUTF8 = [password UTF8String];
193 NSData *passwordBytes = [NSData dataWithBytes:passwordUTF8 length:strlen(passwordUTF8)];
195 // Sometimes securityd crashes between SOSCCRegisterUserCredentials and processRequests
196 // (which results in a process error -- I think this is 13355140), as a workaround we retry
197 // failure a few times before we give up.
198 for (int try = 0; try < 5 && !processed; try++) {
199 if (!SOSCCTryUserCredentials(CFSTR(""), (__bridge CFDataRef)(passwordBytes), &error)) {
200 NSLog(@"Try user credentials failed %@", error);
201 if ((error == NULL) ||
202 (CFEqual(kSOSErrorDomain, CFErrorGetDomain(error)) && kSOSErrorWrongPassword == CFErrorGetCode(error))) {
203 NSLog(@"Calling askAboutAll again...");
204 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
205 Applicant* applicant = (Applicant*) obj;
206 applicant.applicantUIState = ApplicantWaiting;
209 CFReleaseNull(error);
212 EXIT_LOGGED_FAILURE(EX_DATAERR);
215 processed = processRequests(&error);
217 NSLog(@"Can't processRequests: %@ for %@", error, onScreen);
219 CFReleaseNull(error);
222 if (processed && firstApplicantWaitingOrOnScreen()) {
223 cancelCurrentAlert(false);
226 cancelCurrentAlert(true);
231 static void passwordFailurePrompt()
233 NSString *pwIncorrect = [NSString stringWithFormat:(NSString *)CFBridgingRelease(SecCopyCKString(SEC_CK_PASSWORD_INCORRECT)), appleIDAccountName()];
234 NSString *tryAgain = CFBridgingRelease(SecCopyCKString(SEC_CK_TRY_AGAIN));
235 NSDictionary *noteAttributes = @{
236 (id) kCFUserNotificationAlertHeaderKey : pwIncorrect,
237 (id) kCFUserNotificationDefaultButtonTitleKey : tryAgain,
238 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
239 (__bridge id) SBUserNotificationDontDismissOnUnlock: @YES,
240 (__bridge id) SBUserNotificationDismissOnLock : @NO,
242 CFOptionFlags flags = kCFUserNotificationPlainAlertLevel;
244 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef)noteAttributes);
247 CFUserNotificationReceiveResponse(note, 0.0, &flags);
253 static NSString *getLocalizedApprovalBody(NSString *deviceType) {
254 CFStringRef applicationReminder = NULL;
256 if ([deviceType isEqualToString:@"iPhone"])
257 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPHONE);
258 else if ([deviceType isEqualToString:@"iPod"])
259 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPOD);
260 else if ([deviceType isEqualToString:@"iPad"])
261 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPAD);
262 else if ([deviceType isEqualToString:@"Mac"])
263 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_MAC);
265 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_GENERIC);
267 return (__bridge_transfer NSString *) applicationReminder;
271 static NSDictionary *createNote(Applicant *applicantToAskAbout)
273 if(!applicantToAskAbout || !applicantToAskAbout.name || !applicantToAskAbout.deviceType)
276 NSString *header = [NSString stringWithFormat: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVAL_TITLE_IOS), applicantToAskAbout.name];
277 NSString *body = [NSString stringWithFormat: getLocalizedApprovalBody(applicantToAskAbout.deviceType), appleIDAccountName()];
280 (id) kCFUserNotificationAlertHeaderKey : header,
281 (id) kCFUserNotificationAlertMessageKey : body,
282 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_ALLOW),
283 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_DONT_ALLOW),
284 (id) kCFUserNotificationTextFieldTitlesKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_ICLOUD_PASSWORD),
285 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
286 (__bridge_transfer id) SBUserNotificationDontDismissOnUnlock: @YES,
287 (__bridge_transfer id) SBUserNotificationDismissOnLock : @NO,
292 static void askAboutAll(bool passwordFailure)
294 if ([[MCProfileConnection sharedConnection] effectiveBoolValueForSetting: MCFeatureAccountModificationAllowed] == MCRestrictedBoolExplicitNo) {
295 NSLog(@"Account modifications not allowed.");
299 if (passwordFailure) {
300 passwordFailurePrompt();
303 if ((passwordFailure || !currentAlertIsForApplicants) && currentAlert) {
304 if (!currentAlertIsForApplicants) {
305 CFUserNotificationCancel(currentAlert);
307 // after password failure we need to remove the existing alert and supporting objects
308 // because we can't reuse them.
309 CFReleaseNull(currentAlert);
310 CFReleaseNull(currentAlertSource);
312 currentAlertIsForApplicants = true;
314 Applicant *applicantToAskAbout = firstApplicantWaitingOrOnScreen();
315 NSLog(@"Asking about: %@ (of: %@)", applicantToAskAbout, applicants);
317 NSDictionary *noteAttributes = createNote(applicantToAskAbout);
318 if(!noteAttributes) {
319 NSLog(@"NULL data for %@", applicantToAskAbout);
320 cancelCurrentAlert(true);
324 CFOptionFlags flags = flagsForAsk(applicantToAskAbout);
327 SInt32 err = CFUserNotificationUpdate(currentAlert, 0, flags, (__bridge CFDictionaryRef)noteAttributes);
329 NSLog(@"CFUserNotificationUpdate err=%d", (int)err);
330 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
334 currentAlert = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef)(noteAttributes));
336 NSLog(@"Can't make notification for %@ err=%x", applicantToAskAbout, (int)err);
337 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
340 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, applicantChoice, 0);
341 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
344 applicantToAskAbout.applicantUIState = ApplicantOnScreen;
348 static void scheduleActivity(int alertInterval)
350 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
351 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
352 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
353 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
354 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
355 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
357 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
358 NSLog(@"activity handler fired");
363 static void reminderChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
364 if (responseFlags == kCFUserNotificationAlternateResponse || responseFlags == kCFUserNotificationDefaultResponse) {
365 PersistentState *state = [PersistentState loadFromStorage];
366 NSDate *nowish = [NSDate new];
367 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
368 scheduleActivity(state.pendingApplicationReminderAlertInterval);
369 [state writeToStorage];
370 if (responseFlags == kCFUserNotificationAlternateResponse) {
372 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
373 NSLog(@"%s iCSC: opening %@ ok=%d", __FUNCTION__, castleKeychainUrl, ok);
377 cancelCurrentAlert(true);
381 static bool iCloudResetAvailable() {
382 SecureBackup *backupd = [SecureBackup new];
383 NSDictionary *backupdResults;
384 NSError *error = [backupd getAccountInfoWithInfo:nil results:&backupdResults];
385 NSLog(@"SecureBackup e=%@ r=%@", error, backupdResults);
386 return (error == nil && [backupdResults[kSecureBackupIsEnabledKey] isEqualToNumber:@YES]);
391 static NSString *getLocalizedApplicationReminder() {
392 CFStringRef applicationReminder = NULL;
393 switch (MGGetSInt32Answer(kMGQDeviceClassNumber, MGDeviceClassInvalid)) {
394 case MGDeviceClassiPhone:
395 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPHONE);
397 case MGDeviceClassiPod:
398 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPOD);
400 case MGDeviceClassiPad:
401 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPAD);
404 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_GENERIC);
407 return (__bridge_transfer NSString *) applicationReminder;
411 static void postApplicationReminderAlert(NSDate *nowish, PersistentState *state, unsigned int alertInterval)
413 NSString *body = getLocalizedApplicationReminder();
414 bool has_iCSC = iCloudResetAvailable();
416 if (state.defaultPendingApplicationReminderAlertInterval != state.pendingApplicationReminderAlertInterval) {
417 body = [body stringByAppendingFormat: @"〖debug interval %u; wait time %@〗",
418 state.pendingApplicationReminderAlertInterval,
419 [nowish copyDescriptionOfIntervalSince:state.applicationDate]];
422 NSDictionary *pendingAttributes = @{
423 (id) kCFUserNotificationAlertHeaderKey : (__bridge NSString *) SecCopyCKString(SEC_CK_REMINDER_TITLE_IOS),
424 (id) kCFUserNotificationAlertMessageKey : body,
425 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge NSString *) SecCopyCKString(SEC_CK_REMINDER_BUTTON_OK),
426 (id) kCFUserNotificationAlternateButtonTitleKey: has_iCSC ? (__bridge NSString *) SecCopyCKString(SEC_CK_REMINDER_BUTTON_ICSC) : @"",
427 (id) kCFUserNotificationAlertTopMostKey : @YES,
428 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
429 (__bridge id) SBUserNotificationDismissOnLock : @NO,
430 (__bridge id) SBUserNotificationOneButtonPerLine : @YES,
433 currentAlert = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) pendingAttributes);
436 NSLog(@"Can't make pending notification err=%x", (int)err);
438 currentAlertIsForApplicants = false;
439 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, reminderChoice, 0);
440 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
445 static void kickOutChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
446 NSLog(@"kOC %@ %lu", userNotification, responseFlags);
447 if (responseFlags == kCFUserNotificationDefaultResponse) {
448 // We need to let things unwind to main for the new state to get saved
450 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
451 NSLog(@"ok=%d opening %@", ok, [NSURL URLWithString:castleKeychainUrl]);
454 cancelCurrentAlert(true);
458 CFStringRef const CJRAggdDepartureReasonKey = CFSTR("com.apple.security.circlejoinrequested.departurereason");
459 CFStringRef const CJRAggdNumCircleDevicesKey = CFSTR("com.apple.security.circlejoinrequested.numcircledevices");
461 static void postKickedOutAlert(enum DepartureReason reason)
463 NSString *header = nil;
464 NSString *message = nil;
466 // <rdar://problem/20358568> iCDP telemetry: ☂ Statistics on circle reset and drop out events
467 ADClientSetValueForScalarKey(CJRAggdDepartureReasonKey, reason);
469 int64_t num_peers = 0;
470 CFArrayRef peerList = SOSCCCopyPeerPeerInfo(NULL);
472 num_peers = CFArrayGetCount(peerList);
473 if (num_peers > 99) {
474 // Round down # peers to 2 significant digits
476 for (factor = 10; num_peers >= 100*factor; factor *= 10) ;
477 num_peers = (num_peers / factor) * factor;
481 ADClientSetValueForScalarKey(CJRAggdNumCircleDevicesKey, num_peers);
483 debugState = @"pKOA A";
484 syslog(LOG_ERR, "DepartureReason %d", reason);
486 case kSOSDiscoveredRetirement:
487 case kSOSLostPrivateKey:
488 case kSOSWithdrewMembership:
489 // Was: SEC_CK_CR_BODY_WITHDREW
490 // "... if you turn off a switch you have some idea why the light is off" - Murf
494 case kSOSNeverAppliedToCircle:
495 // We didn't get kicked out, we were never here. This should only happen if we changed iCloud accounts
496 // (and we had sync on in the previous one, and never had it on in the new one). As this is explicit
497 // user action alot of the "Light switch" argument (above) applies.
501 case kSOSNeverLeftCircle:
502 case kSOSMembershipRevoked:
503 case kSOSLeftUntrustedCircle:
505 header = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_TITLE);
506 message = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_BODY_IOS);
510 if (CPIsInternalDevice()) {
511 static const char *departureReasonStrings[] = {
512 "kSOSDepartureReasonError",
513 "kSOSNeverLeftCircle",
514 "kSOSWithdrewMembership",
515 "kSOSMembershipRevoked",
516 "kSOSLeftUntrustedCircle",
517 "kSOSNeverAppliedToCircle",
518 "kSOSDiscoveredRetirement",
519 "kSOSLostPrivateKey",
522 int idx = (kSOSDepartureReasonError <= reason && reason <= kSOSLostPrivateKey) ? reason : (kSOSLostPrivateKey + 1);
523 NSString *reason_str = [NSString stringWithFormat:(__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CR_REASON_INTERNAL),
524 departureReasonStrings[idx]];
525 message = [message stringByAppendingString: reason_str];
528 NSDictionary *kickedAttributes = @{
529 (id) kCFUserNotificationAlertHeaderKey : header,
530 (id) kCFUserNotificationAlertMessageKey : message,
531 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE),
532 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW),
533 (id) kCFUserNotificationAlertTopMostKey : @YES,
534 (__bridge id) SBUserNotificationDismissOnLock : @NO,
535 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
536 (__bridge id) SBUserNotificationOneButtonPerLine : @YES,
540 if (currentAlertIsForKickOut) {
541 debugState = @"pKOA B";
542 NSLog(@"Updating existing alert %@ with %@", currentAlert, kickedAttributes);
543 CFUserNotificationUpdate(currentAlert, 0, kCFUserNotificationPlainAlertLevel, (__bridge CFDictionaryRef) kickedAttributes);
545 debugState = @"pKOA C";
547 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) kickedAttributes);
548 assert((note == NULL) == (err != 0));
550 NSLog(@"Can't make kicked out notification err=%x", (int)err);
552 currentAlertIsForApplicants = false;
553 currentAlertIsForKickOut = true;
556 NSLog(@"New ko alert %@ a=%@", currentAlert, kickedAttributes);
557 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, kickOutChoice, 0);
558 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
559 int backupStateChangeToken;
560 notify_register_dispatch("com.apple.EscrowSecurityAlert.reset", &backupStateChangeToken, dispatch_get_main_queue(), ^(int token) {
561 if (currentAlert == note) {
562 NSLog(@"Backup state might have changed (dS=%@)", debugState);
563 postKickedOutAlert(reason);
565 NSLog(@"Backup state may have changed, but we don't care anymore (dS=%@)", debugState);
568 debugState = @"pKOA D";
570 debugState = @"pKOA E";
571 notify_cancel(backupStateChangeToken);
574 debugState = @"pKOA Z";
578 static bool processEvents()
580 debugState = @"processEvents A";
582 CFErrorRef error = NULL;
583 CFErrorRef departError = NULL;
584 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&error);
585 NSDate *nowish = [NSDate date];
586 PersistentState *state = [PersistentState loadFromStorage];
587 enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError);
588 NSLog(@"CircleStatus %d -> %d{%d} (s=%p)", state.lastCircleStatus, circleStatus, departureReason, state);
591 // Pending application reminder
592 NSTimeInterval timeUntilApplicationAlert = [state.pendingApplicationReminder timeIntervalSinceDate:nowish];
593 NSLog(@"Time until pendingApplicationReminder (%@) %f", [state.pendingApplicationReminder debugDescription], timeUntilApplicationAlert);
594 if (circleStatus == kSOSCCRequestPending) {
595 if (timeUntilApplicationAlert <= 0) {
596 debugState = @"reminderAlert";
597 postApplicationReminderAlert(nowish, state, state.pendingApplicationReminderAlertInterval);
599 scheduleActivity(ceil(timeUntilApplicationAlert));
604 // No longer in circle?
605 if ((state.lastCircleStatus == kSOSCCInCircle && (circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent)) ||
606 (state.lastCircleStatus == kSOSCCCircleAbsent && circleStatus == kSOSCCNotInCircle && state.absentCircleWithNoReason) ||
607 state.debugShowLeftReason) {
608 // Used to be in the circle, now we aren't - tell the user why
609 debugState = @"processEvents B";
611 if (state.debugShowLeftReason) {
612 NSLog(@"debugShowLeftReason: %@", state.debugShowLeftReason);
613 departureReason = [state.debugShowLeftReason intValue];
614 state.debugShowLeftReason = nil;
615 CFReleaseNull(departError);
616 [state writeToStorage];
619 if (departureReason != kSOSDepartureReasonError) {
620 state.absentCircleWithNoReason = (circleStatus == kSOSCCCircleAbsent && departureReason == kSOSNeverLeftCircle);
621 NSLog(@"Depature reason %d", departureReason);
622 postKickedOutAlert(departureReason);
623 NSLog(@"pKOA returned (cS %d lCS %d)", circleStatus, state.lastCircleStatus);
625 NSLog(@"Couldn't get last departure reason: %@", departError);
630 // Circle applications: pending request(s) started / completed
631 debugState = @"processEvents C";
632 if (circleStatus != state.lastCircleStatus) {
633 SOSCCStatus lastCircleStatus = state.lastCircleStatus;
634 state.lastCircleStatus = circleStatus;
636 if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
637 NSLog(@"Pending request started");
638 state.applicationDate = nowish;
639 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
640 scheduleActivity(state.pendingApplicationReminderAlertInterval);
642 if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
643 NSLog(@"Pending request completed");
644 state.applicationDate = [NSDate distantPast];
645 state.pendingApplicationReminder = [NSDate distantFuture];
648 [state writeToStorage];
651 if (circleStatus != kSOSCCInCircle) {
652 if (circleStatus == kSOSCCRequestPending && currentAlert) {
654 CFUserNotificationRef postedAlert = currentAlert;
656 debugState = @"processEvents D1";
657 notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ifyToken, dispatch_get_main_queue(), ^(int token) {
658 if (postedAlert != currentAlert) {
659 NSLog(@"-- CC after original alert gone (currentAlertIsForApplicants %d, pA %p, cA %p -- %@)",
660 currentAlertIsForApplicants, postedAlert, currentAlert, currentAlert);
661 notify_cancel(token);
663 CFErrorRef localError = NULL;
664 SOSCCStatus newCircleStatus = SOSCCThisDeviceIsInCircle(&localError);
665 if (newCircleStatus != kSOSCCRequestPending) {
666 if (newCircleStatus == kSOSCCError)
667 NSLog(@"No longer pending (nCS=%d, alert=%@) error: %@", newCircleStatus, currentAlert, localError);
669 NSLog(@"No longer pending (nCS=%d, alert=%@)", newCircleStatus, currentAlert);
670 cancelCurrentAlert(true);
672 NSLog(@"Still pending...");
674 CFReleaseNull(localError);
677 debugState = @"processEvents D2";
678 NSLog(@"NOTE: currentAlertIsForApplicants %d, token %d", currentAlertIsForApplicants, notifyToken);
682 debugState = @"processEvents D4";
683 NSLog(@"SOSCCThisDeviceIsInCircle status %d, not checking applicants", circleStatus);
689 debugState = @"processEvents E";
690 applicants = [NSMutableDictionary new];
691 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(&error)) {
692 Applicant *applicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
693 applicants[applicant.idString] = applicant;
696 // Log error from SOSCCCopyApplicantPeerInfo() above?
697 CFReleaseNull(error);
699 int notify_token = -42;
700 debugState = @"processEvents F";
701 int notify_register_status = notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ify_token, dispatch_get_main_queue(), ^(int token) {
702 NSLog(@"Notified: %s", kSOSCCCircleChangedNotification);
703 CFErrorRef circleStatusError = NULL;
705 bool needsUpdate = false;
706 CFErrorRef copyPeerError = NULL;
707 NSMutableSet *newIds = [NSMutableSet new];
708 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(©PeerError)) {
709 Applicant *newApplicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
710 [newIds addObject:newApplicant.idString];
711 Applicant *existingApplicant = applicants[newApplicant.idString];
712 if (existingApplicant) {
713 switch (existingApplicant.applicantUIState) {
714 case ApplicantWaiting:
715 applicants[newApplicant.idString] = newApplicant;
718 case ApplicantOnScreen:
719 newApplicant.applicantUIState = ApplicantOnScreen;
720 applicants[newApplicant.idString] = newApplicant;
724 NSLog(@"Update to %@ >> %@ with pending order, should work out ok though", existingApplicant, newApplicant);
729 applicants[newApplicant.idString] = newApplicant;
733 NSLog(@"Could not update peer info array: %@", copyPeerError);
734 CFRelease(copyPeerError);
738 NSMutableArray *idsToRemoveFromApplicants = [NSMutableArray new];
739 for (NSString *exisitngId in [applicants keyEnumerator]) {
740 if (![newIds containsObject:exisitngId]) {
741 [idsToRemoveFromApplicants addObject:exisitngId];
745 [applicants removeObjectsForKeys:idsToRemoveFromApplicants];
747 if (newIds.count == 0) {
748 NSLog(@"All applicants were handled elsewhere");
749 cancelCurrentAlert(true);
751 SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&circleStatusError);
752 if (kSOSCCInCircle != currentCircleStatus) {
753 NSLog(@"Left circle (%d), not handling remaining %lu applicants", currentCircleStatus, (unsigned long)newIds.count);
754 cancelCurrentAlert(true);
759 NSLog(@"needsUpdate false, not updating alert");
761 // Log circleStatusError?
762 CFReleaseNull(circleStatusError);
764 NSLog(@"ACC token %d, status %d", notify_token, notify_register_status);
765 debugState = @"processEvents F2";
767 if (applicants.count == 0) {
768 NSLog(@"No applicants");
770 debugState = @"processEvents F3";
772 debugState = @"processEvents F4";
774 debugState = @"processEvents F5";
779 debugState = @"processEvents F6";
780 notify_cancel(notify_token);
781 debugState = @"processEvents DONE";
787 int main (int argc, const char * argv[]) {
788 xpc_transaction_begin();
791 // NOTE: DISPATCH_QUEUE_PRIORITY_LOW will not actually manage to drain events in a lot of cases (like circleStatus != kSOSCCInCircle)
792 xpc_set_event_stream_handler("com.apple.notifyd.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t object) {
793 char *event_description = xpc_copy_description(object);
794 NSLog(@"notifyd event: %s\nAlert (%p) %s %s\ndebugState: %@", event_description, currentAlert,
795 currentAlertIsForApplicants ? "for applicants" : "!applicants",
796 currentAlertIsForKickOut ? "KO" : "!KO", debugState);
797 free(event_description);
800 xpc_activity_register(kLaunchLaterXPCName, XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
804 while (falseInARow < 2) {
805 if (processEvents()) {
810 cancelCurrentAlert(false);
811 if (doOnceInMainBlockChain) {
812 doOnceInMainBlockChain();
813 doOnceInMainBlockChain = NULL;
819 xpc_transaction_end();