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 if ([toAccept count]) {
118 NSLog(@"Process accept: %@", toAccept);
119 ok = ok && SOSCCAcceptApplicants((__bridge CFArrayRef) toAccept, error);
121 NSLog(@"kSOSCCHoldLockForInitialSync");
122 notify_post(kSOSCCHoldLockForInitialSync);
126 if ([toReject count]) {
127 NSLog(@"Process reject: %@", toReject);
128 ok = ok && SOSCCRejectApplicants((__bridge CFArrayRef) toReject, error);
135 static void cancelCurrentAlert(bool stopRunLoop) {
136 if (currentAlertSource) {
137 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
138 CFReleaseNull(currentAlertSource);
141 CFUserNotificationCancel(currentAlert);
142 CFReleaseNull(currentAlert);
145 CFRunLoopStop(CFRunLoopGetCurrent());
147 currentAlertIsForKickOut = currentAlertIsForApplicants = false;
151 static void askAboutAll(bool passwordFailure);
154 static void applicantChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
156 ApplicantUIState choice;
158 if (kCFUserNotificationAlternateResponse == responseFlags) {
159 choice = ApplicantRejected;
160 } else if (kCFUserNotificationDefaultResponse == responseFlags) {
161 choice = ApplicantAccepted;
163 NSLog(@"Unexpected response %lu", responseFlags);
164 choice = ApplicantRejected;
168 CFErrorRef error = NULL;
169 NSArray *onScreen = applicantsInState(ApplicantOnScreen);
171 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
172 Applicant* applicant = (Applicant *) obj;
173 applicant.applicantUIState = choice;
176 if (choice == ApplicantRejected) {
177 // If this device has ever set up the public key this should work without the password...
178 processed = processRequests(&error);
180 NSLog(@"Didn't need password to process %@", onScreen);
181 cancelCurrentAlert(true);
184 // ...however if the public key gets lost we should "just" fall through to the validate
186 NSLog(@"Couldn't process reject without password (e=%@) for %@ (will try with password next)", error, onScreen);
188 CFReleaseNull(error);
191 NSString *password = (__bridge NSString *) CFUserNotificationGetResponseValue(userNotification, kCFUserNotificationTextFieldValuesKey, 0);
193 NSLog(@"No password given, retry");
197 const char *passwordUTF8 = [password UTF8String];
198 NSData *passwordBytes = [NSData dataWithBytes:passwordUTF8 length:strlen(passwordUTF8)];
200 // Sometimes securityd crashes between SOSCCRegisterUserCredentials and processRequests
201 // (which results in a process error -- I think this is 13355140), as a workaround we retry
202 // failure a few times before we give up.
203 for (int try = 0; try < 5 && !processed; try++) {
204 if (!SOSCCTryUserCredentials(CFSTR(""), (__bridge CFDataRef) passwordBytes, &error)) {
205 NSLog(@"Try user credentials failed %@", error);
206 if ((error == NULL) ||
207 (CFEqual(kSOSErrorDomain, CFErrorGetDomain(error)) && kSOSErrorWrongPassword == CFErrorGetCode(error))) {
208 NSLog(@"Calling askAboutAll again...");
209 [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
210 Applicant *applicant = (Applicant*) obj;
211 applicant.applicantUIState = ApplicantWaiting;
214 CFReleaseNull(error);
217 EXIT_LOGGED_FAILURE(EX_DATAERR);
220 processed = processRequests(&error);
222 NSLog(@"Can't processRequests: %@ for %@", error, onScreen);
224 CFReleaseNull(error);
227 if (processed && firstApplicantWaitingOrOnScreen()) {
228 cancelCurrentAlert(false);
231 cancelCurrentAlert(true);
236 static void passwordFailurePrompt()
238 NSString *pwIncorrect = [NSString stringWithFormat:(NSString *)CFBridgingRelease(SecCopyCKString(SEC_CK_PASSWORD_INCORRECT)), appleIDAccountName()];
239 NSString *tryAgain = CFBridgingRelease(SecCopyCKString(SEC_CK_TRY_AGAIN));
240 NSDictionary *noteAttributes = @{
241 (id) kCFUserNotificationAlertHeaderKey : pwIncorrect,
242 (id) kCFUserNotificationDefaultButtonTitleKey : tryAgain,
243 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
244 (__bridge id) SBUserNotificationDontDismissOnUnlock: @YES,
245 (__bridge id) SBUserNotificationDismissOnLock : @NO,
247 CFOptionFlags flags = kCFUserNotificationPlainAlertLevel;
249 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
252 CFUserNotificationReceiveResponse(note, 0.0, &flags);
258 static NSString *getLocalizedApprovalBody(NSString *deviceType) {
259 CFStringRef applicationReminder = NULL;
261 if ([deviceType isEqualToString:@"iPhone"])
262 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPHONE);
263 else if ([deviceType isEqualToString:@"iPod"])
264 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPOD);
265 else if ([deviceType isEqualToString:@"iPad"])
266 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_IPAD);
267 else if ([deviceType isEqualToString:@"Mac"])
268 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_MAC);
270 applicationReminder = SecCopyCKString(SEC_CK_APPROVAL_BODY_IOS_GENERIC);
272 return (__bridge_transfer NSString *) applicationReminder;
276 static NSDictionary *createNote(Applicant *applicantToAskAbout)
278 if(!applicantToAskAbout || !applicantToAskAbout.name || !applicantToAskAbout.deviceType)
281 NSString *header = [NSString stringWithFormat: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVAL_TITLE_IOS), applicantToAskAbout.name];
282 NSString *body = [NSString stringWithFormat: getLocalizedApprovalBody(applicantToAskAbout.deviceType), appleIDAccountName()];
285 (id) kCFUserNotificationAlertHeaderKey : header,
286 (id) kCFUserNotificationAlertMessageKey : body,
287 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_ALLOW),
288 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_DONT_ALLOW),
289 (id) kCFUserNotificationTextFieldTitlesKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_ICLOUD_PASSWORD),
290 (id) kCFUserNotificationAlertTopMostKey : @YES, // get us onto the lock screen
291 (__bridge_transfer id) SBUserNotificationDontDismissOnUnlock: @YES,
292 (__bridge_transfer id) SBUserNotificationDismissOnLock : @NO,
297 static void askAboutAll(bool passwordFailure)
299 if ([[MCProfileConnection sharedConnection] effectiveBoolValueForSetting: MCFeatureAccountModificationAllowed] == MCRestrictedBoolExplicitNo) {
300 NSLog(@"Account modifications not allowed.");
304 if (passwordFailure) {
305 passwordFailurePrompt();
308 if ((passwordFailure || !currentAlertIsForApplicants) && currentAlert) {
309 if (!currentAlertIsForApplicants) {
310 CFUserNotificationCancel(currentAlert);
312 // after password failure we need to remove the existing alert and supporting objects
313 // because we can't reuse them.
314 CFReleaseNull(currentAlert);
315 CFReleaseNull(currentAlertSource);
317 currentAlertIsForApplicants = true;
319 Applicant *applicantToAskAbout = firstApplicantWaitingOrOnScreen();
320 NSLog(@"Asking about: %@ (of: %@)", applicantToAskAbout, applicants);
322 NSDictionary *noteAttributes = createNote(applicantToAskAbout);
323 if(!noteAttributes) {
324 NSLog(@"NULL data for %@", applicantToAskAbout);
325 cancelCurrentAlert(true);
329 CFOptionFlags flags = flagsForAsk(applicantToAskAbout);
332 SInt32 err = CFUserNotificationUpdate(currentAlert, 0, flags, (__bridge CFDictionaryRef) noteAttributes);
334 NSLog(@"CFUserNotificationUpdate err=%d", (int)err);
335 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
339 currentAlert = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef) noteAttributes);
341 NSLog(@"Can't make notification for %@ err=%x", applicantToAskAbout, (int)err);
342 EXIT_LOGGED_FAILURE(EX_SOFTWARE);
345 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, applicantChoice, 0);
346 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
349 applicantToAskAbout.applicantUIState = ApplicantOnScreen;
353 static void scheduleActivity(int alertInterval)
355 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
356 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
357 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
358 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
359 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
360 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
362 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
363 NSLog(@"activity handler fired");
368 static void reminderChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
369 if (responseFlags == kCFUserNotificationAlternateResponse || responseFlags == kCFUserNotificationDefaultResponse) {
370 PersistentState *state = [PersistentState loadFromStorage];
371 NSDate *nowish = [NSDate new];
372 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
373 scheduleActivity(state.pendingApplicationReminderAlertInterval);
374 [state writeToStorage];
375 if (responseFlags == kCFUserNotificationAlternateResponse) {
377 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
378 NSLog(@"%s iCSC: opening %@ ok=%d", __FUNCTION__, castleKeychainUrl, ok);
382 cancelCurrentAlert(true);
386 static bool iCloudResetAvailable() {
387 SecureBackup *backupd = [SecureBackup new];
388 NSDictionary *backupdResults;
389 NSError *error = [backupd getAccountInfoWithInfo:nil results:&backupdResults];
390 NSLog(@"SecureBackup e=%@ r=%@", error, backupdResults);
391 return (error == nil && [backupdResults[kSecureBackupIsEnabledKey] isEqualToNumber:@YES]);
396 static NSString *getLocalizedApplicationReminder() {
397 CFStringRef applicationReminder = NULL;
398 switch (MGGetSInt32Answer(kMGQDeviceClassNumber, MGDeviceClassInvalid)) {
399 case MGDeviceClassiPhone:
400 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPHONE);
402 case MGDeviceClassiPod:
403 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPOD);
405 case MGDeviceClassiPad:
406 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_IPAD);
409 applicationReminder = SecCopyCKString(SEC_CK_REMINDER_BODY_IOS_GENERIC);
412 return (__bridge_transfer NSString *) applicationReminder;
416 static void postApplicationReminderAlert(NSDate *nowish, PersistentState *state, unsigned int alertInterval)
418 NSString *body = getLocalizedApplicationReminder();
419 bool has_iCSC = iCloudResetAvailable();
421 if (CPIsInternalDevice() &&
422 state.defaultPendingApplicationReminderAlertInterval != state.pendingApplicationReminderAlertInterval) {
423 body = [body stringByAppendingFormat: @"〖debug interval %u; wait time %@〗",
424 state.pendingApplicationReminderAlertInterval,
425 [nowish copyDescriptionOfIntervalSince:state.applicationDate]];
428 NSDictionary *pendingAttributes = @{
429 (id) kCFUserNotificationAlertHeaderKey : (__bridge NSString *) SecCopyCKString(SEC_CK_REMINDER_TITLE_IOS),
430 (id) kCFUserNotificationAlertMessageKey : body,
431 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge NSString *) SecCopyCKString(SEC_CK_REMINDER_BUTTON_OK),
432 (id) kCFUserNotificationAlternateButtonTitleKey: has_iCSC ? (__bridge NSString *) SecCopyCKString(SEC_CK_REMINDER_BUTTON_ICSC) : @"",
433 (id) kCFUserNotificationAlertTopMostKey : @YES,
434 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
435 (__bridge id) SBUserNotificationDismissOnLock : @NO,
436 (__bridge id) SBUserNotificationOneButtonPerLine : @YES,
439 currentAlert = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) pendingAttributes);
442 NSLog(@"Can't make pending notification err=%x", (int)err);
444 currentAlertIsForApplicants = false;
445 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, reminderChoice, 0);
446 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
451 static void kickOutChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
452 NSLog(@"kOC %@ %lu", userNotification, responseFlags);
453 if (responseFlags == kCFUserNotificationDefaultResponse) {
454 // We need to let things unwind to main for the new state to get saved
456 BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
457 NSLog(@"ok=%d opening %@", ok, [NSURL URLWithString:castleKeychainUrl]);
460 cancelCurrentAlert(true);
464 CFStringRef const CJRAggdDepartureReasonKey = CFSTR("com.apple.security.circlejoinrequested.departurereason");
465 CFStringRef const CJRAggdNumCircleDevicesKey = CFSTR("com.apple.security.circlejoinrequested.numcircledevices");
467 static void postKickedOutAlert(enum DepartureReason reason)
469 NSString *header = nil;
470 NSString *message = nil;
472 // <rdar://problem/20358568> iCDP telemetry: ☂ Statistics on circle reset and drop out events
473 ADClientSetValueForScalarKey(CJRAggdDepartureReasonKey, reason);
475 int64_t num_peers = 0;
476 CFArrayRef peerList = SOSCCCopyPeerPeerInfo(NULL);
478 num_peers = CFArrayGetCount(peerList);
479 if (num_peers > 99) {
480 // Round down # peers to 2 significant digits
482 for (factor = 10; num_peers >= 100*factor; factor *= 10) ;
483 num_peers = (num_peers / factor) * factor;
487 ADClientSetValueForScalarKey(CJRAggdNumCircleDevicesKey, num_peers);
489 debugState = @"pKOA A";
490 syslog(LOG_ERR, "DepartureReason %d", reason);
492 case kSOSDiscoveredRetirement:
493 case kSOSLostPrivateKey:
494 case kSOSWithdrewMembership:
495 // Was: SEC_CK_CR_BODY_WITHDREW
496 // "... if you turn off a switch you have some idea why the light is off" - Murf
500 case kSOSNeverAppliedToCircle:
501 // We didn't get kicked out, we were never here. This should only happen if we changed iCloud accounts
502 // (and we had sync on in the previous one, and never had it on in the new one). As this is explicit
503 // user action alot of the "Light switch" argument (above) applies.
507 case kSOSNeverLeftCircle:
508 case kSOSMembershipRevoked:
509 case kSOSLeftUntrustedCircle:
511 header = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_TITLE);
512 message = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_BODY_IOS);
516 if (CPIsInternalDevice()) {
517 static const char *departureReasonStrings[] = {
518 "kSOSDepartureReasonError",
519 "kSOSNeverLeftCircle",
520 "kSOSWithdrewMembership",
521 "kSOSMembershipRevoked",
522 "kSOSLeftUntrustedCircle",
523 "kSOSNeverAppliedToCircle",
524 "kSOSDiscoveredRetirement",
525 "kSOSLostPrivateKey",
528 int idx = (kSOSDepartureReasonError <= reason && reason <= kSOSLostPrivateKey) ? reason : (kSOSLostPrivateKey + 1);
529 NSString *reason_str = [NSString stringWithFormat:(__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CR_REASON_INTERNAL),
530 departureReasonStrings[idx]];
531 message = [message stringByAppendingString: reason_str];
534 NSDictionary *kickedAttributes = @{
535 (id) kCFUserNotificationAlertHeaderKey : header,
536 (id) kCFUserNotificationAlertMessageKey : message,
537 (id) kCFUserNotificationDefaultButtonTitleKey : (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE),
538 (id) kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW),
539 (id) kCFUserNotificationAlertTopMostKey : @YES,
540 (__bridge id) SBUserNotificationDismissOnLock : @NO,
541 (__bridge id) SBUserNotificationDontDismissOnUnlock : @YES,
542 (__bridge id) SBUserNotificationOneButtonPerLine : @YES,
546 if (currentAlertIsForKickOut) {
547 debugState = @"pKOA B";
548 NSLog(@"Updating existing alert %@ with %@", currentAlert, kickedAttributes);
549 CFUserNotificationUpdate(currentAlert, 0, kCFUserNotificationPlainAlertLevel, (__bridge CFDictionaryRef) kickedAttributes);
551 debugState = @"pKOA C";
553 CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef) kickedAttributes);
554 assert((note == NULL) == (err != 0));
556 NSLog(@"Can't make kicked out notification err=%x", (int)err);
558 currentAlertIsForApplicants = false;
559 currentAlertIsForKickOut = true;
562 NSLog(@"New ko alert %@ a=%@", currentAlert, kickedAttributes);
563 currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, kickOutChoice, 0);
564 CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
565 int backupStateChangeToken;
566 notify_register_dispatch("com.apple.EscrowSecurityAlert.reset", &backupStateChangeToken, dispatch_get_main_queue(), ^(int token) {
567 if (currentAlert == note) {
568 NSLog(@"Backup state might have changed (dS=%@)", debugState);
569 postKickedOutAlert(reason);
571 NSLog(@"Backup state may have changed, but we don't care anymore (dS=%@)", debugState);
574 debugState = @"pKOA D";
576 debugState = @"pKOA E";
577 notify_cancel(backupStateChangeToken);
580 debugState = @"pKOA Z";
584 static bool processEvents()
586 debugState = @"processEvents A";
588 CFErrorRef error = NULL;
589 CFErrorRef departError = NULL;
590 SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&error);
591 NSDate *nowish = [NSDate date];
592 PersistentState *state = [PersistentState loadFromStorage];
593 enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError);
594 NSLog(@"CircleStatus %d -> %d{%d} (s=%p)", state.lastCircleStatus, circleStatus, departureReason, state);
597 // Pending application reminder
598 NSTimeInterval timeUntilApplicationAlert = [state.pendingApplicationReminder timeIntervalSinceDate:nowish];
599 NSLog(@"Time until pendingApplicationReminder (%@) %f", [state.pendingApplicationReminder debugDescription], timeUntilApplicationAlert);
600 if (circleStatus == kSOSCCRequestPending) {
601 if (timeUntilApplicationAlert <= 0) {
602 debugState = @"reminderAlert";
603 postApplicationReminderAlert(nowish, state, state.pendingApplicationReminderAlertInterval);
605 scheduleActivity(ceil(timeUntilApplicationAlert));
610 // No longer in circle?
611 if ((state.lastCircleStatus == kSOSCCInCircle && (circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent)) ||
612 (state.lastCircleStatus == kSOSCCCircleAbsent && circleStatus == kSOSCCNotInCircle && state.absentCircleWithNoReason) ||
613 state.debugShowLeftReason) {
614 // Used to be in the circle, now we aren't - tell the user why
615 debugState = @"processEvents B";
617 if (state.debugShowLeftReason) {
618 NSLog(@"debugShowLeftReason: %@", state.debugShowLeftReason);
619 departureReason = [state.debugShowLeftReason intValue];
620 state.debugShowLeftReason = nil;
621 CFReleaseNull(departError);
622 [state writeToStorage];
625 if (departureReason != kSOSDepartureReasonError) {
626 state.absentCircleWithNoReason = (circleStatus == kSOSCCCircleAbsent && departureReason == kSOSNeverLeftCircle);
627 NSLog(@"Depature reason %d", departureReason);
628 postKickedOutAlert(departureReason);
629 NSLog(@"pKOA returned (cS %d lCS %d)", circleStatus, state.lastCircleStatus);
631 NSLog(@"Couldn't get last departure reason: %@", departError);
636 // Circle applications: pending request(s) started / completed
637 debugState = @"processEvents C";
638 if (circleStatus != state.lastCircleStatus) {
639 SOSCCStatus lastCircleStatus = state.lastCircleStatus;
640 state.lastCircleStatus = circleStatus;
642 if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
643 NSLog(@"Pending request started");
644 state.applicationDate = nowish;
645 state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
646 scheduleActivity(state.pendingApplicationReminderAlertInterval);
648 if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
649 NSLog(@"Pending request completed");
650 state.applicationDate = [NSDate distantPast];
651 state.pendingApplicationReminder = [NSDate distantFuture];
654 [state writeToStorage];
657 if (circleStatus != kSOSCCInCircle) {
658 if (circleStatus == kSOSCCRequestPending && currentAlert) {
660 CFUserNotificationRef postedAlert = currentAlert;
662 debugState = @"processEvents D1";
663 notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ifyToken, dispatch_get_main_queue(), ^(int token) {
664 if (postedAlert != currentAlert) {
665 NSLog(@"-- CC after original alert gone (currentAlertIsForApplicants %d, pA %p, cA %p -- %@)",
666 currentAlertIsForApplicants, postedAlert, currentAlert, currentAlert);
667 notify_cancel(token);
669 CFErrorRef localError = NULL;
670 SOSCCStatus newCircleStatus = SOSCCThisDeviceIsInCircle(&localError);
671 if (newCircleStatus != kSOSCCRequestPending) {
672 if (newCircleStatus == kSOSCCError)
673 NSLog(@"No longer pending (nCS=%d, alert=%@) error: %@", newCircleStatus, currentAlert, localError);
675 NSLog(@"No longer pending (nCS=%d, alert=%@)", newCircleStatus, currentAlert);
676 cancelCurrentAlert(true);
678 NSLog(@"Still pending...");
680 CFReleaseNull(localError);
683 debugState = @"processEvents D2";
684 NSLog(@"NOTE: currentAlertIsForApplicants %d, token %d", currentAlertIsForApplicants, notifyToken);
688 debugState = @"processEvents D4";
689 NSLog(@"SOSCCThisDeviceIsInCircle status %d, not checking applicants", circleStatus);
695 debugState = @"processEvents E";
696 applicants = [NSMutableDictionary new];
697 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(&error)) {
698 Applicant *applicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
699 applicants[applicant.idString] = applicant;
702 // Log error from SOSCCCopyApplicantPeerInfo() above?
703 CFReleaseNull(error);
705 int notify_token = -42;
706 debugState = @"processEvents F";
707 int notify_register_status = notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ify_token, dispatch_get_main_queue(), ^(int token) {
708 NSLog(@"Notified: %s", kSOSCCCircleChangedNotification);
709 CFErrorRef circleStatusError = NULL;
711 bool needsUpdate = false;
712 CFErrorRef copyPeerError = NULL;
713 NSMutableSet *newIds = [NSMutableSet new];
714 for (id applicantInfo in (__bridge_transfer NSArray *) SOSCCCopyApplicantPeerInfo(©PeerError)) {
715 Applicant *newApplicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef) applicantInfo];
716 [newIds addObject:newApplicant.idString];
717 Applicant *existingApplicant = applicants[newApplicant.idString];
718 if (existingApplicant) {
719 switch (existingApplicant.applicantUIState) {
720 case ApplicantWaiting:
721 applicants[newApplicant.idString] = newApplicant;
724 case ApplicantOnScreen:
725 newApplicant.applicantUIState = ApplicantOnScreen;
726 applicants[newApplicant.idString] = newApplicant;
730 NSLog(@"Update to %@ >> %@ with pending order, should work out ok though", existingApplicant, newApplicant);
735 applicants[newApplicant.idString] = newApplicant;
739 NSLog(@"Could not update peer info array: %@", copyPeerError);
740 CFRelease(copyPeerError);
744 NSMutableArray *idsToRemoveFromApplicants = [NSMutableArray new];
745 for (NSString *exisitngId in [applicants keyEnumerator]) {
746 if (![newIds containsObject:exisitngId]) {
747 [idsToRemoveFromApplicants addObject:exisitngId];
751 [applicants removeObjectsForKeys:idsToRemoveFromApplicants];
753 if (newIds.count == 0) {
754 NSLog(@"All applicants were handled elsewhere");
755 cancelCurrentAlert(true);
757 SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&circleStatusError);
758 if (kSOSCCInCircle != currentCircleStatus) {
759 NSLog(@"Left circle (%d), not handling remaining %lu applicants", currentCircleStatus, (unsigned long)newIds.count);
760 cancelCurrentAlert(true);
765 NSLog(@"needsUpdate false, not updating alert");
767 // Log circleStatusError?
768 CFReleaseNull(circleStatusError);
770 NSLog(@"ACC token %d, status %d", notify_token, notify_register_status);
771 debugState = @"processEvents F2";
773 if (applicants.count == 0) {
774 NSLog(@"No applicants");
776 debugState = @"processEvents F3";
778 debugState = @"processEvents F4";
780 debugState = @"processEvents F5";
785 debugState = @"processEvents F6";
786 notify_cancel(notify_token);
787 debugState = @"processEvents DONE";
793 int main (int argc, const char * argv[]) {
794 xpc_transaction_begin();
797 // NOTE: DISPATCH_QUEUE_PRIORITY_LOW will not actually manage to drain events in a lot of cases (like circleStatus != kSOSCCInCircle)
798 xpc_set_event_stream_handler("com.apple.notifyd.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t object) {
799 char *event_description = xpc_copy_description(object);
800 NSLog(@"notifyd event: %s\nAlert (%p) %s %s\ndebugState: %@", event_description, currentAlert,
801 currentAlertIsForApplicants ? "for applicants" : "!applicants",
802 currentAlertIsForKickOut ? "KO" : "!KO", debugState);
803 free(event_description);
806 xpc_activity_register(kLaunchLaterXPCName, XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
810 while (falseInARow < 2) {
811 if (processEvents()) {
816 cancelCurrentAlert(false);
817 if (doOnceInMainBlockChain) {
818 doOnceInMainBlockChain();
819 doOnceInMainBlockChain = NULL;
825 xpc_transaction_end();