+//
+// CircleJoinRequested.m
+// CircleJoinRequested
+//
+// Created by J Osborne on 3/5/13.
+// Copyright (c) 2013-2014 Apple Inc. All Rights Reserved.
+//
+#import <Accounts/Accounts.h>
+#import <Accounts/ACAccountStore_Private.h>
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnewline-eof"
+#import <AppleAccount/AppleAccount.h>
+#pragma clang diagnostic pop
+#import <AppleAccount/ACAccountStore+AppleAccount.h>
+#import <Accounts/ACAccountType_Private.h>
+#import <Foundation/Foundation.h>
+#include <dispatch/dispatch.h>
+#include "SecureObjectSync/SOSCloudCircle.h"
+#include "SecureObjectSync/SOSPeerInfo.h"
+#import <CoreFoundation/CFUserNotification.h>
+#import <SpringBoardServices/SBSCFUserNotificationKeys.h>
+#include <notify.h>
+#include <sysexits.h>
+#import "Applicant.h"
+#import "NSArray+map.h"
+#import <ManagedConfiguration/MCProfileConnection.h>
+#import <ManagedConfiguration/MCFeatures.h>
+#import <Security/SecFrameworkStrings.h>
+#import "PersistantState.h"
+#include <xpc/private.h>
+#include <sys/time.h>
+#import "NSDate+TimeIntervalDescription.h"
+#include <MobileGestalt.h>
+#include <xpc/activity.h>
+#include <xpc/private.h>
+#import <MobileCoreServices/MobileCoreServices.h>
+#import <MobileCoreServices/LSApplicationWorkspace.h>
+#import <CloudServices/SecureBackup.h>
+#import <syslog.h>
+
+// As long as we are logging the failure use exit code of zero to make launchd happy
+#define EXIT_LOGGED_FAILURE(code) xpc_transaction_end(); exit(0)
+
+const char *kLaunchLaterXPCName = "com.apple.security.CircleJoinRequestedTick";
+CFRunLoopSourceRef currentAlertSource = NULL;
+CFUserNotificationRef currentAlert = NULL;
+bool currentAlertIsForApplicants = true;
+bool currentAlertIsForKickOut = false;
+NSMutableDictionary *applicants = nil;
+volatile NSString *debugState = @"main?";
+dispatch_block_t doOnceInMainBlockChain = NULL;
+
+NSString *castleKeychainUrl = @"prefs:root=CASTLE&path=Keychain/ADVANCED";
+
+#if 0
+// For use with: __attribute__((cleanup(CFReleaseSafeIndirect))) CFType auto_var;
+static void CFReleaseSafeIndirect(void *o_)
+{
+ void **o = o_;
+ if (o && *o) {
+ CFRelease(*o);
+ }
+}
+#endif
+
+static void doOnceInMain(dispatch_block_t block)
+{
+ if (doOnceInMainBlockChain) {
+ doOnceInMainBlockChain = ^{
+ doOnceInMainBlockChain();
+ block();
+ };
+ } else {
+ doOnceInMainBlockChain = block;
+ }
+}
+
+static NSString *appleIDAccountName()
+{
+ ACAccountStore *accountStore = [[ACAccountStore alloc] init];
+ ACAccount *primaryAppleAccount = [accountStore aa_primaryAppleAccount];
+ return primaryAppleAccount.username;
+}
+
+static CFOptionFlags flagsForAsk(Applicant *applicant)
+{
+ return kCFUserNotificationPlainAlertLevel|CFUserNotificationSecureTextField(0);
+}
+
+// NOTE: gives precedence to OnScreen
+static Applicant *firstApplicantWaitingOrOnScreen()
+{
+ Applicant *waiting = nil;
+ for (Applicant *applicant in [applicants objectEnumerator]) {
+ if (applicant.applicantUIState == ApplicantOnScreen) {
+ return applicant;
+ } else if (applicant.applicantUIState == ApplicantWaiting) {
+ waiting = applicant;
+ }
+ }
+
+ return waiting;
+}
+
+static NSMutableArray *applicantsInState(ApplicantUIState state)
+{
+ NSMutableArray *results = [NSMutableArray new];
+ for (Applicant *applicant in [applicants objectEnumerator]) {
+ if (applicant.applicantUIState == state) {
+ [results addObject:applicant];
+ }
+ }
+
+ return results;
+}
+
+static BOOL processRequests(CFErrorRef *error)
+{
+ bool ok = true;
+ NSMutableArray *toAccept = [[applicantsInState(ApplicantAccepted) mapWithBlock:^id(id obj) {
+ return (id)[obj rawPeerInfo];
+ }] mutableCopy];
+ NSMutableArray *toReject = [[applicantsInState(ApplicantRejected) mapWithBlock:^id(id obj) {
+ return (id)[obj rawPeerInfo];
+ }] mutableCopy];
+
+ NSLog(@"Process accept: %@", toAccept);
+ NSLog(@"Process reject: %@", toReject);
+
+ if ([toAccept count]) {
+ ok = ok && SOSCCAcceptApplicants((__bridge CFArrayRef)(toAccept), error);
+ }
+ if ([toReject count]) {
+ ok = ok && SOSCCRejectApplicants((__bridge CFArrayRef)(toReject), error);
+ }
+
+ return ok;
+}
+
+static void cancelCurrentAlert(bool stopRunLoop)
+{
+ if (currentAlertSource) {
+ CFRunLoopRemoveSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
+ CFRelease(currentAlertSource);
+ currentAlertSource = NULL;
+ }
+ if (currentAlert) {
+ CFUserNotificationCancel(currentAlert);
+ CFRelease(currentAlert);
+ currentAlert = NULL;
+ }
+ if (stopRunLoop) {
+ CFRunLoopStop(CFRunLoopGetCurrent());
+ }
+ currentAlertIsForKickOut = currentAlertIsForApplicants = false;
+}
+
+static void askAboutAll(bool passwordFailure);
+
+static void applicantChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
+{
+ ApplicantUIState choice;
+
+ if (kCFUserNotificationAlternateResponse == responseFlags) {
+ choice = ApplicantRejected;
+ } else if (kCFUserNotificationDefaultResponse == responseFlags) {
+ choice = ApplicantAccepted;
+ } else {
+ NSLog(@"Unexpected response %lu", responseFlags);
+ choice = ApplicantRejected;
+ }
+
+ BOOL processed = NO;
+ CFErrorRef error = NULL;
+
+ NSArray *onScreen = applicantsInState(ApplicantOnScreen);
+
+ [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
+ Applicant* applicant = (Applicant*) obj;
+
+ applicant.applicantUIState = choice;
+ }];
+
+ if (choice == ApplicantRejected) {
+ // If this device has ever set up the public key this should work without the password...
+ processed = processRequests(&error);
+ if (processed) {
+ NSLog(@"Didn't need password to process %@", onScreen);
+ cancelCurrentAlert(true);
+ return;
+ } else {
+ // ...however if the public key gets lost we should "just" fall through to the validate
+ // password path.
+ NSLog(@"Couldn't process reject without password (e=%@) for %@ (will try with password next)", error, onScreen);
+ }
+ }
+
+ NSString *password = (__bridge NSString *)(CFUserNotificationGetResponseValue(userNotification, kCFUserNotificationTextFieldValuesKey, 0));
+ if (!password) {
+ NSLog(@"No password given, retry");
+ askAboutAll(true);
+ return;
+ }
+ const char *passwordUTF8 = [password UTF8String];
+ NSData *passwordBytes = [NSData dataWithBytes:passwordUTF8 length:strlen(passwordUTF8)];
+
+ // Sometimes securityd crashes between the SOSCCRegisterUserCredentials and the processRequests,
+ // (which results in a process error -- I think this is 13355140); as a workaround we retry
+ // failure a few times before we give up.
+ for (int try = 0; try < 5 && !processed; try++) {
+ if (!SOSCCTryUserCredentials(CFSTR(""), (__bridge CFDataRef)(passwordBytes), &error)) {
+ NSLog(@"Try user credentials failed %@", error);
+ if ((error==NULL) || (CFEqual(kSOSErrorDomain, CFErrorGetDomain(error)) && kSOSErrorWrongPassword == CFErrorGetCode(error))) {
+ NSLog(@"Calling askAboutAll again...");
+
+ [onScreen enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
+ Applicant* applicant = (Applicant*) obj;
+
+ applicant.applicantUIState = ApplicantWaiting;
+ }];
+ askAboutAll(true);
+ return;
+ }
+ EXIT_LOGGED_FAILURE(EX_DATAERR);
+ }
+
+ processed = processRequests(&error);
+ if (!processed) {
+ NSLog(@"Can't processRequests: %@ for %@", error, onScreen);
+ }
+ }
+ if (processed && firstApplicantWaitingOrOnScreen()) {
+ cancelCurrentAlert(false);
+ askAboutAll(false);
+ } else {
+ cancelCurrentAlert(true);
+ }
+}
+
+static void passwordFailurePrompt()
+{
+ //CFBridgingRelease
+ NSString *pwIncorrect = [NSString stringWithFormat:(NSString *)CFBridgingRelease(SecCopyCKString(SEC_CK_PASSWORD_INCORRECT)), appleIDAccountName()];
+ NSString *tryAgain = CFBridgingRelease(SecCopyCKString(SEC_CK_TRY_AGAIN));
+ NSDictionary *noteAttributes = @{
+ (id)kCFUserNotificationAlertHeaderKey: pwIncorrect,
+ (id)kCFUserNotificationDefaultButtonTitleKey: tryAgain,
+ // TopMost gets us onto the lock screen
+ (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
+ (__bridge id)SBUserNotificationDontDismissOnUnlock: @YES,
+ (__bridge id)SBUserNotificationDismissOnLock: @NO,
+ };
+ CFOptionFlags flags = kCFUserNotificationPlainAlertLevel;
+ SInt32 err;
+ CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef)noteAttributes);
+ CFUserNotificationReceiveResponse(note, 0.0, &flags);
+ CFRelease(note);
+}
+
+static NSDictionary *createNote(Applicant *applicantToAskAbout) {
+ if(!applicantToAskAbout) return NULL;
+ NSString *appName = applicantToAskAbout.name;
+ if(!appName) return NULL;
+ NSString *devType = applicantToAskAbout.deviceType;
+ if(!devType) return NULL;
+ return @{
+ (id)kCFUserNotificationAlertHeaderKey: [NSString stringWithFormat:(__bridge_transfer NSString*)SecCopyCKString(SEC_CK_JOIN_TITLE), appName],
+ (id)kCFUserNotificationAlertMessageKey: [NSString stringWithFormat:(__bridge_transfer NSString*)SecCopyCKString(SEC_CK_JOIN_PROMPT), appleIDAccountName(), devType],
+ (id)kCFUserNotificationDefaultButtonTitleKey: (__bridge_transfer NSString*)SecCopyCKString(SEC_CK_ALLOW),
+ (id)kCFUserNotificationAlternateButtonTitleKey: (__bridge_transfer NSString*)SecCopyCKString(SEC_CK_DONT_ALLOW),
+ (id)kCFUserNotificationTextFieldTitlesKey: (__bridge_transfer NSString*)SecCopyCKString(SEC_CK_ICLOUD_PASSWORD),
+ // TopMost gets us onto the lock screen
+ (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
+ (__bridge_transfer id)SBUserNotificationDontDismissOnUnlock: @YES,
+ (__bridge_transfer id)SBUserNotificationDismissOnLock: @NO,
+ };
+}
+
+static void askAboutAll(bool passwordFailure)
+{
+ if ([[MCProfileConnection sharedConnection] effectiveBoolValueForSetting: MCFeatureAccountModificationAllowed] == MCRestrictedBoolExplicitNo) {
+ NSLog(@"Account modifications not allowed.");
+ return;
+ }
+
+ if (passwordFailure) {
+ passwordFailurePrompt();
+ }
+
+ if ((passwordFailure || !currentAlertIsForApplicants) && currentAlert) {
+ if (!currentAlertIsForApplicants) {
+ CFUserNotificationCancel(currentAlert);
+ }
+ // after password failure we need to remove the existing alert and supporting objects
+ // because we can't reuse them.
+ CFRelease(currentAlert);
+ currentAlert = NULL;
+ if (currentAlertSource) {
+ CFRelease(currentAlertSource);
+ currentAlertSource = NULL;
+ }
+ }
+ currentAlertIsForApplicants = true;
+
+ Applicant *applicantToAskAbout = firstApplicantWaitingOrOnScreen();
+ NSLog(@"Asking about: %@ (of: %@)", applicantToAskAbout, applicants);
+
+ NSDictionary *noteAttributes = createNote(applicantToAskAbout);
+ if(!noteAttributes) {
+ NSLog(@"NULL data for %@", applicantToAskAbout);
+ cancelCurrentAlert(true);
+ return;
+ }
+
+ CFOptionFlags flags = flagsForAsk(applicantToAskAbout);
+
+ if (currentAlert) {
+ SInt32 err = CFUserNotificationUpdate(currentAlert, 0, flags, (__bridge CFDictionaryRef)noteAttributes);
+ if (err) {
+ NSLog(@"CFUserNotificationUpdate err=%d", (int)err);
+ EXIT_LOGGED_FAILURE(EX_SOFTWARE);
+ }
+ } else {
+ SInt32 err = 0;
+ currentAlert = CFUserNotificationCreate(NULL, 0.0, flags, &err, (__bridge CFDictionaryRef)(noteAttributes));
+ if (err) {
+ NSLog(@"Can't make notification for %@ err=%x", applicantToAskAbout, (int)err);
+ EXIT_LOGGED_FAILURE(EX_SOFTWARE);
+ }
+
+ currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, applicantChoice, 0);
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
+ }
+
+ applicantToAskAbout.applicantUIState = ApplicantOnScreen;
+}
+
+static void scheduleActivity(int alertInterval)
+{
+ xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
+ xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
+ xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
+ xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
+ xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
+ xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
+
+ xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
+ NSLog(@"activity handler fired");
+ });
+}
+
+static void reminderChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
+{
+ if (kCFUserNotificationAlternateResponse == responseFlags || kCFUserNotificationDefaultResponse == responseFlags) {
+ PersistantState *state = [PersistantState loadFromStorage];
+ NSDate *nowish = [NSDate new];
+ state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
+ scheduleActivity(state.pendingApplicationReminderAlertInterval);
+ [state writeToStorage];
+ if (kCFUserNotificationAlternateResponse == responseFlags) {
+ BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
+ NSLog(@"ok=%d opening %@", ok, [NSURL URLWithString:castleKeychainUrl]);
+ }
+ }
+
+ cancelCurrentAlert(true);
+}
+
+
+static NSString* getLocalizedDeviceClass(void) {
+ NSString *deviceType = NULL;
+ switch (MGGetSInt32Answer(kMGQDeviceClassNumber, MGDeviceClassInvalid)) {
+ case MGDeviceClassiPhone:
+ deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_IPHONE);
+ break;
+ case MGDeviceClassiPod:
+ deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_IPOD);
+ break;
+ case MGDeviceClassiPad:
+ deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_IPAD);
+ break;
+ default:
+ deviceType = (__bridge NSString*)SecCopyCKString(SEC_CK_THIS_DEVICE);
+ break;
+ }
+ return deviceType;
+}
+
+static bool iCloudResetAvailable()
+{
+ SecureBackup *backupd = [SecureBackup new];
+ NSDictionary *backupdResults;
+ NSError *error = [backupd getAccountInfoWithInfo:nil results:&backupdResults];
+ NSLog(@"SecureBackup e=%@ r=%@", error, backupdResults);
+ return (nil == error && [backupdResults[kSecureBackupIsEnabledKey] isEqualToNumber:@YES]);
+}
+
+static void postApplicationReminderAlert(NSDate *nowish, PersistantState *state, unsigned int alertInterval)
+{
+
+ NSString *deviceType = getLocalizedDeviceClass();
+
+ bool has_iCSC = iCloudResetAvailable();
+
+ NSString *alertMessage = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(has_iCSC ? SEC_CK_ARS1_BODY : SEC_CK_ARS0_BODY), deviceType];
+ if (state.defaultPendingApplicationReminderAlertInterval != state.pendingApplicationReminderAlertInterval) {
+ alertMessage = [NSString stringWithFormat:@"%@ 〖debug interval %u; wait time %@〗",
+ alertMessage,
+ state.pendingApplicationReminderAlertInterval,
+ [nowish copyDescriptionOfIntervalSince:state.applcationDate]];
+ }
+
+ NSDictionary *pendingAttributes = @{
+ (id)kCFUserNotificationAlertHeaderKey: (__bridge NSString*)SecCopyCKString(has_iCSC ? SEC_CK_ARS1_TITLE : SEC_CK_ARS0_TITLE),
+ (id)kCFUserNotificationAlertMessageKey: alertMessage,
+ (id)kCFUserNotificationDefaultButtonTitleKey: (__bridge NSString*)SecCopyCKString(SEC_CK_AR_APPROVE_OTHER),
+ (id)kCFUserNotificationAlternateButtonTitleKey: has_iCSC ? (__bridge NSString*)SecCopyCKString(SEC_CK_AR_USE_CODE) : @"",
+ (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
+ (__bridge id)SBUserNotificationHideButtonsInAwayView: @YES,
+ (__bridge id)SBUserNotificationDontDismissOnUnlock: @YES,
+ (__bridge id)SBUserNotificationDismissOnLock: @NO,
+ (__bridge id)SBUserNotificationOneButtonPerLine: @YES,
+ };
+ SInt32 err = 0;
+ currentAlert = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef)(pendingAttributes));
+
+ if (err) {
+ NSLog(@"Can't make pending notification err=%x", (int)err);
+ } else {
+ currentAlertIsForApplicants = false;
+ currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, reminderChoice, 0);
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
+ }
+}
+
+static void kickOutChoice(CFUserNotificationRef userNotification, CFOptionFlags responseFlags)
+{
+ NSLog(@"kOC %@ %lu", userNotification, responseFlags);
+ if (kCFUserNotificationAlternateResponse == responseFlags) {
+ // We need to let things unwind to main for the new state to get saved
+ doOnceInMain(^{
+ BOOL ok = [[LSApplicationWorkspace defaultWorkspace] openSensitiveURL:[NSURL URLWithString:castleKeychainUrl] withOptions:nil];
+ NSLog(@"ok=%d opening %@", ok, [NSURL URLWithString:castleKeychainUrl]);
+ });
+ }
+ cancelCurrentAlert(true);
+}
+
+static void postKickedOutAlert(enum DepartureReason reason)
+{
+ NSString *deviceType = getLocalizedDeviceClass();
+ NSString *message = nil;
+ debugState = @"pKOA A";
+ bool ok_to_use_code = iCloudResetAvailable();
+ debugState = @"pKOA B";
+
+ switch (reason) {
+ case kSOSNeverLeftCircle:
+ // Was: SEC_CK_CR_BODY_NEVER_LEFT
+ return;
+ break;
+
+ case kSOSWithdrewMembership:
+ // Was: SEC_CK_CR_BODY_WITHDREW
+ // "... if you turn off a switch you have some idea why the light is off" - Murf
+ return;
+ break;
+
+ case kSOSMembershipRevoked:
+ message = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(SEC_CK_CR_BODY_REVOKED), deviceType];
+ break;
+
+ case kSOSLeftUntrustedCircle:
+ message = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(SEC_CK_CR_BODY_LEFT_UNTRUSTED), deviceType];
+ ok_to_use_code = false;
+ break;
+
+ case kSOSNeverAppliedToCircle:
+ // We didn't get kicked out, we were never here. This should only happen if we changed iCloud accounts
+ // (and we had sync on in the previous one, and never had it on in the new one). As this is explicit
+ // user action alot of thd "Light switch" argument (above) applies.
+ return;
+ break;
+
+ default:
+ message = [NSString stringWithFormat:(__bridge NSString*)SecCopyCKString(SEC_CK_CR_BODY_UNKNOWN), deviceType];
+ ok_to_use_code = false;
+ syslog(LOG_ERR, "Unknown DepartureReason %d", reason);
+ break;
+ }
+
+ NSDictionary *kickedAttributes = @{
+ (id)kCFUserNotificationAlertHeaderKey: (__bridge NSString*)SecCopyCKString(SEC_CK_CR_TITLE),
+ (id)kCFUserNotificationAlertMessageKey: message,
+ (id)kCFUserNotificationDefaultButtonTitleKey: (__bridge NSString*)SecCopyCKString(SEC_CK_CR_OK),
+ (id)kCFUserNotificationAlternateButtonTitleKey: ok_to_use_code ? (__bridge NSString*)SecCopyCKString(SEC_CK_CR_USE_CODE)
+ : @"",
+ (id)kCFUserNotificationAlertTopMostKey: (id)kCFBooleanTrue,
+ (__bridge id)SBUserNotificationHideButtonsInAwayView: @YES,
+ (__bridge id)SBUserNotificationDontDismissOnUnlock: @YES,
+ (__bridge id)SBUserNotificationDismissOnLock: @NO,
+ (__bridge id)SBUserNotificationOneButtonPerLine: @YES,
+ };
+ SInt32 err = 0;
+
+ if (currentAlertIsForKickOut) {
+ debugState = @"pKOA C";
+ NSLog(@"Updating existing alert %@ with %@", currentAlert, kickedAttributes);
+ CFUserNotificationUpdate(currentAlert, 0, kCFUserNotificationPlainAlertLevel, (__bridge CFDictionaryRef)(kickedAttributes));
+ } else {
+ debugState = @"pKOA D";
+
+ CFUserNotificationRef note = CFUserNotificationCreate(NULL, 0.0, kCFUserNotificationPlainAlertLevel, &err, (__bridge CFDictionaryRef)(kickedAttributes));
+ if (err) {
+ NSLog(@"Can't make kicked out notification err=%x", (int)err);
+ } else {
+ currentAlertIsForApplicants = false;
+ currentAlertIsForKickOut = true;
+
+ currentAlert = note;
+ NSLog(@"New ko alert %@ a=%@", currentAlert, kickedAttributes);
+ currentAlertSource = CFUserNotificationCreateRunLoopSource(NULL, currentAlert, kickOutChoice, 0);
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), currentAlertSource, kCFRunLoopDefaultMode);
+ int backupStateChangeToken;
+ notify_register_dispatch("com.apple.EscrowSecurityAlert.reset", &backupStateChangeToken, dispatch_get_main_queue(), ^(int token) {
+ if (currentAlert == note) {
+ NSLog(@"Backup state might have changed (dS=%@)", debugState);
+ postKickedOutAlert(reason);
+ } else {
+ NSLog(@"Backup state may have changed, but we don't care anymore (dS=%@)", debugState);
+ }
+ });
+ debugState = @"pKOA E";
+ CFRunLoopRun();
+ debugState = @"pKOA F";
+ notify_cancel(backupStateChangeToken);
+ }
+ }
+ debugState = @"pKOA Z";
+}
+
+static bool processEvents()
+{
+ debugState = @"processEvents A";
+ CFErrorRef error = NULL;
+ CFErrorRef departError = NULL;
+ SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&error);
+ NSDate *nowish = [NSDate date];
+ PersistantState *state = [PersistantState loadFromStorage];
+ enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError);
+ NSLog(@"CircleStatus %d -> %d{%d} (s=%p)", state.lastCircleStatus, circleStatus, departureReason, state);
+
+ NSTimeInterval timeUntilApplicationAlert = [state.pendingApplicationReminder timeIntervalSinceDate:nowish];
+
+ NSLog(@"Time until pendingApplicationReminder (%@) %f", [state.pendingApplicationReminder debugDescription], timeUntilApplicationAlert);
+
+ if (circleStatus == kSOSCCRequestPending && timeUntilApplicationAlert <= 0) {
+ debugState = @"reminderAlert";
+ postApplicationReminderAlert(nowish, state, state.pendingApplicationReminderAlertInterval);
+ } else if (circleStatus == kSOSCCRequestPending) {
+ scheduleActivity(ceil(timeUntilApplicationAlert));
+ }
+
+ if (((circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent) && state.lastCircleStatus == kSOSCCInCircle) || state.debugShowLeftReason || (circleStatus == kSOSCCNotInCircle && state.lastCircleStatus == kSOSCCCircleAbsent && state.absentCircleWithNoReason)) {
+ debugState = @"processEvents B";
+ // Use to be in the circle, now we aren't. We ought to tell the user why.
+
+ if (state.debugShowLeftReason) {
+ NSLog(@"debugShowLeftReason is %@", state.debugShowLeftReason);
+ departureReason = [state.debugShowLeftReason intValue];
+ state.debugShowLeftReason = nil;
+ departError = NULL;
+ [state writeToStorage];
+ }
+
+ if (kSOSDepartureReasonError != departureReason) {
+ if (circleStatus == kSOSCCCircleAbsent && departureReason == kSOSNeverLeftCircle) {
+ // We don't yet know why the circle has vanished, remember our current ignorance
+ state.absentCircleWithNoReason = YES;
+ } else {
+ state.absentCircleWithNoReason = NO;
+ }
+ NSLog(@"Depature reason %d", departureReason);
+ postKickedOutAlert(departureReason);
+ NSLog(@"pKOA returned (cS %d lCS %d)", circleStatus, state.lastCircleStatus);
+ } else {
+ NSLog(@"Can't get last depature reason: %@", departError);
+ }
+ }
+
+ debugState = @"processEvents C";
+
+ if (circleStatus != state.lastCircleStatus) {
+ SOSCCStatus lastCircleStatus = state.lastCircleStatus;
+ state.lastCircleStatus = circleStatus;
+
+ if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
+ state.applcationDate = nowish;
+ state.pendingApplicationReminder = [nowish dateByAddingTimeInterval: state.pendingApplicationReminderAlertInterval];
+ scheduleActivity(state.pendingApplicationReminderAlertInterval);
+ }
+ if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
+ NSLog(@"Pending request completed");
+ state.applcationDate = [NSDate distantPast];
+ state.pendingApplicationReminder = [NSDate distantFuture];
+ }
+
+ [state writeToStorage];
+ }
+
+ if (circleStatus != kSOSCCInCircle) {
+ if (circleStatus == kSOSCCRequestPending && currentAlert) {
+ int notifyToken = 0;
+ CFUserNotificationRef postedAlert = currentAlert;
+
+ debugState = @"processEvents D1";
+ notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ifyToken, dispatch_get_main_queue(), ^(int token) {
+ if (postedAlert != currentAlert) {
+ NSLog(@"-- CC after original alert gone (currentAlertIsForApplicants %d, pA %p, cA %p -- %@)", currentAlertIsForApplicants ? 1:0, postedAlert, currentAlert, currentAlert);
+ notify_cancel(token);
+ } else {
+ CFErrorRef localError = NULL;
+ SOSCCStatus newCircleStatus = SOSCCThisDeviceIsInCircle(&localError);
+ if (newCircleStatus != kSOSCCRequestPending) {
+ if (newCircleStatus == kSOSCCError)
+ NSLog(@"No longer pending (nCS=%d, alert=%@) error: %@", newCircleStatus, currentAlert, localError);
+ else
+ NSLog(@"No longer pending (nCS=%d, alert=%@)", newCircleStatus, currentAlert);
+ cancelCurrentAlert(true);
+ } else {
+ NSLog(@"Still pending...");
+ }
+ }
+ });
+ debugState = @"processEvents D2";
+ NSLog(@"NOTE: currentAlertIsForApplicants %d, token %d", currentAlertIsForApplicants ? 1:0, notifyToken);
+ CFRunLoopRun();
+ return true;
+ }
+ debugState = @"processEvents D3";
+ NSLog(@"SOSCCThisDeviceIsInCircle status %d, not checking applicants", circleStatus);
+ return false;
+ }
+
+ debugState = @"processEvents E";
+ applicants = [NSMutableDictionary new];
+ for (id applicantInfo in (__bridge_transfer NSArray *)(SOSCCCopyApplicantPeerInfo(&error))) {
+ Applicant *applicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef)(applicantInfo)];
+ applicants[applicant.idString] = applicant;
+ }
+
+ int notify_token = -42;
+ debugState = @"processEvents F";
+ int notify_register_status = notify_register_dispatch(kSOSCCCircleChangedNotification, ¬ify_token, dispatch_get_main_queue(), ^(int token) {
+ NSLog(@"Notified: %s", kSOSCCCircleChangedNotification);
+ CFErrorRef circleStatusError = NULL;
+
+ bool needsUpdate = false;
+ CFErrorRef copyPeerError = NULL;
+ NSMutableSet *newIds = [NSMutableSet new];
+ for (id applicantInfo in (__bridge_transfer NSArray *)(SOSCCCopyApplicantPeerInfo(©PeerError))) {
+ Applicant *newApplicant = [[Applicant alloc] initWithPeerInfo:(__bridge SOSPeerInfoRef)(applicantInfo)];
+ [newIds addObject:newApplicant.idString];
+ Applicant *existingApplicant = applicants[newApplicant.idString];
+ if (existingApplicant) {
+ switch (existingApplicant.applicantUIState) {
+ case ApplicantWaiting:
+ applicants[newApplicant.idString] = newApplicant;
+ break;
+
+ case ApplicantOnScreen:
+ newApplicant.applicantUIState = ApplicantOnScreen;
+ applicants[newApplicant.idString] = newApplicant;
+ break;
+
+ default:
+ NSLog(@"Update to %@ >> %@ with pending order, should work out Ok though", existingApplicant, newApplicant);
+ break;
+ }
+ } else {
+ needsUpdate = true;
+ applicants[newApplicant.idString] = newApplicant;
+ }
+ }
+ if (copyPeerError) {
+ NSLog(@"Could not update peer info array: %@", copyPeerError);
+ return;
+ }
+
+ NSMutableArray *idsToRemoveFromApplicants = [NSMutableArray new];
+ for (NSString *exisitngId in [applicants keyEnumerator]) {
+ if (![newIds containsObject:exisitngId]) {
+ [idsToRemoveFromApplicants addObject:exisitngId];
+ needsUpdate = true;
+ }
+ }
+ [applicants removeObjectsForKeys:idsToRemoveFromApplicants];
+
+ if (newIds.count == 0) {
+ NSLog(@"All applicants were handled elsewhere");
+ cancelCurrentAlert(true);
+ }
+ SOSCCStatus currentCircleStatus = SOSCCThisDeviceIsInCircle(&circleStatusError);
+ if (kSOSCCInCircle != currentCircleStatus) {
+ NSLog(@"Left circle (%d), not handing remaining %lu applicants", currentCircleStatus, (unsigned long)newIds.count);
+ cancelCurrentAlert(true);
+ }
+ if (needsUpdate) {
+ askAboutAll(false);
+ } else {
+ NSLog(@"needsUpdate false, not updating alert");
+ }
+ });
+ NSLog(@"ACC token %d, status %d", notify_token, notify_register_status);
+ debugState = @"processEvents F2";
+
+ if (applicants.count == 0) {
+ NSLog(@"No applicants");
+ } else {
+ debugState = @"processEvents F3";
+ askAboutAll(false);
+ debugState = @"processEvents F4";
+ if (currentAlert) {
+ debugState = @"processEvents F5";
+ CFRunLoopRun();
+ }
+ }
+
+ debugState = @"processEvents F6";
+ notify_cancel(notify_token);
+ debugState = @"processEvents DONE";
+
+ return false;
+}
+
+int main (int argc, const char * argv[])
+{
+ @autoreleasepool {
+ xpc_transaction_begin();
+
+ // NOTE: DISPATCH_QUEUE_PRIORITY_LOW will not actually manage to drain events
+ // in a lot of cases (like circleStatus != kSOSCCInCircle)
+ xpc_set_event_stream_handler("com.apple.notifyd.matching", dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(xpc_object_t object) {
+ char *event_description = xpc_copy_description(object);
+ NSLog(@"notifyd event: %s\nAlert (%p) %s %s\ndebugState: %@", event_description, currentAlert, currentAlertIsForApplicants ? "for applicants" : "!applicants", currentAlertIsForKickOut ? "KO" : "!KO", debugState);
+ free(event_description);
+ });
+
+ xpc_activity_register(kLaunchLaterXPCName, XPC_ACTIVITY_CHECK_IN, ^(xpc_activity_t activity) {
+ });
+
+
+ int falseInARow = 0;
+ while (falseInARow < 2) {
+ if (processEvents()) {
+ falseInARow = 0;
+ } else {
+ falseInARow++;
+ }
+ cancelCurrentAlert(false);
+ if (doOnceInMainBlockChain) {
+ doOnceInMainBlockChain();
+ doOnceInMainBlockChain = NULL;
+ }
+ }
+ }
+
+ NSLog(@"Done");
+ xpc_transaction_end();
+ return(0);
+}