//
// KNAppDelegate.m
// Keychain Circle Notification
//
// Created by J Osborne on 2/21/13.
//
//
#import "KNAppDelegate.h"
#import "KDSecCircle.h"
#import "KDCirclePeer.h"
#import "NSDictionary+compactDescription.h"
#import <AOSUI/NSImageAdditions.h>
#import <AppleSystemInfo/AppleSystemInfo.h>
#import <Security/SecFrameworkStrings.h>
#import <AOSAccounts/MobileMePrefsCoreAEPrivate.h>
#import <AOSAccounts/MobileMePrefsCore.h>
static char *kLaunchLaterXPCName = "com.apple.security.Keychain-Circle-Notification-TICK";
@implementation KNAppDelegate
static NSUserNotificationCenter *appropriateNotificationCenter()
{
return [NSUserNotificationCenter _centerForIdentifier:@"com.apple.security.keychain-circle-notification" type:_NSUserNotificationCenterTypeSystem];
}
-(void)notifyiCloudPreferencesAbout:(NSString *)eventName;
{
if (nil == eventName) {
return;
}
NSString *account = (__bridge NSString *)(MMCopyLoggedInAccount());
NSLog(@"notifyiCloudPreferencesAbout
AEDesc aeDesc;
BOOL createdAEDesc = createAEDescWithAEActionAndAccountID((__bridge NSString *)kMMServiceIDKeychainSync, eventName, account, &aeDesc);
if (createdAEDesc)
{
OSErr err;
LSLaunchURLSpec lsSpec;
lsSpec.appURL = NULL;
lsSpec.itemURLs = (__bridge CFArrayRef)([NSArray arrayWithObject:[NSURL fileURLWithPath:@"/System/Library/PreferencePanes/iCloudPref.prefPane"]]);
lsSpec.passThruParams = &aeDesc;
lsSpec.launchFlags = kLSLaunchDefaults | kLSLaunchAsync;
lsSpec.asyncRefCon = NULL;
err = LSOpenFromURLSpec(&lsSpec, NULL);
if (err) {
NSLog(@"Can't send event }
AEDisposeDesc(&aeDesc);
}
else
{
NSLog(@"unable to create and send aedesc for account: '%@' and action: '%@'\n", account, eventName);
}
}
-(void)showiCloudPrefrences
{
static NSAppleScript *script = nil;
if (!script) {
script = [[NSAppleScript alloc] initWithSource:@"tell application \"System Preferences\"\n\
activate\n\
set the current pane to pane id \"com.apple.preferences.icloud\"\n\
end tell"];
}
NSDictionary *appleScriptError = nil;
[script executeAndReturnError:&appleScriptError];
if (appleScriptError) {
NSLog(@"appleScriptError: } else {
NSLog(@"NO appleScript error");
}
}
-(void)timerCheck
{
NSDate *nowish = [NSDate new];
self.state = [KNPersistantState loadFromStorage];
if ([nowish compare:self.state.pendingApplicationReminder] != NSOrderedAscending) {
NSLog(@"REMINDER TIME: // self.circle.rawStatus might not be valid yet
if (SOSCCThisDeviceIsInCircle(NULL) == kSOSCCRequestPending) {
// Still have a request pending, send reminder, and also in addtion to the UI
// we need to send a notification for iCloud pref pane to pick up
CFNotificationCenterPostNotificationWithOptions(CFNotificationCenterGetDistributedCenter(), CFSTR("com.apple.security.secureobjectsync.pendingApplicationReminder"), (__bridge const void *)([self.state.applcationDate description]), NULL, 0);
[self postApplicationReminder];
self.state.pendingApplicationReminder = [self.state.applcationDate dateByAddingTimeInterval:[self getPendingApplicationReminderInterval]];
[self.state writeToStorage];
}
}
}
-(void)scheduleActivityAt:(NSDate*)time
{
if ([time compare:[NSDate distantFuture]] != NSOrderedSame) {
NSTimeInterval howSoon = [time timeIntervalSinceNow];
if (howSoon > 0) {
[self scheduleActivityIn:howSoon];
} else {
[self timerCheck];
}
}
}
-(void)scheduleActivityIn:(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) {
[self timerCheck];
});
}
-(NSTimeInterval)getPendingApplicationReminderInterval
{
if (self.state.pendingApplicationReminderInterval) {
return [self.state.pendingApplicationReminderInterval doubleValue];
} else {
return 48*24*60*60;
}
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
appropriateNotificationCenter().delegate = self;
NSLog(@"Posted at launch:
self.viewedIds = [NSMutableSet new];
self.circle = [KDSecCircle new];
self.state = [KNPersistantState loadFromStorage];
KNAppDelegate *me = self;
[self.circle addChangeCallback:^{
me.state = [KNPersistantState loadFromStorage];
if ((me.state.lastCircleStatus == kSOSCCInCircle && !me.circle.isInCircle) || me.state.debugLeftReason) {
enum DepartureReason reason = kSOSNeverLeftCircle;
if (me.state.debugLeftReason) {
reason = [me.state.debugLeftReason intValue];
me.state.debugLeftReason = nil;
} else {
CFErrorRef err = NULL;
reason = SOSCCGetLastDepartureReason(&err);
if (reason == kSOSDepartureReasonError) {
NSLog(@"SOSCCGetLastDepartureReason err: }
}
//NSString *model = (__bridge NSString *)(ASI_CopyComputerModelName(FALSE));
NSString *body = nil;
switch (reason) {
case kSOSDepartureReasonError:
case kSOSNeverLeftCircle:
case kSOSWithdrewMembership:
break;
default:
NSLog(@"Unknown departure reason // fallthrough on purpose
case kSOSMembershipRevoked:
case kSOSLeftUntrustedCircle:
body = NSLocalizedString(@"Approve this Mac from another device to use iCloud Keychain.", @"Body for iCloud Keychain Reset notification");
break;
}
[me.state writeToStorage];
NSLog(@"departure reason if (body) {
[me postKickedOutWithMessage: body];
}
}
[me timerCheck];
if (me.state.lastCircleStatus != kSOSCCRequestPending && me.circle.rawStatus == kSOSCCRequestPending) {
NSLog(@"Entered RequestPending");
NSDate *nowish = [NSDate new];
me.state.applcationDate = nowish;
me.state.pendingApplicationReminder = [me.state.applcationDate dateByAddingTimeInterval:[me getPendingApplicationReminderInterval]];
[me.state writeToStorage];
[me scheduleActivityAt:me.state.pendingApplicationReminder];
}
NSMutableSet *applicantIds = [NSMutableSet new];
for (KDCirclePeer *applicant in me.circle.applicants) {
if (!me.circle.isInCircle) {
// We don't want to yammer on about circles we aren't in,
// and we don't want to be extra confusing announcing our
// own join requests as if the user could approve them
// locally!
break;
}
[me postForApplicant:applicant];
[applicantIds addObject:applicant.idString];
}
NSUserNotificationCenter *notificationCenter = appropriateNotificationCenter();
NSLog(@"Checking validity of for (NSUserNotification *note in notificationCenter.deliveredNotifications) {
if (note.userInfo[@"applicantId"] && ![applicantIds containsObject:note.userInfo[@"applicantId"]]) {
NSLog(@"No longer an applicant ( [notificationCenter removeDeliveredNotification:note];
} else {
NSLog(@"Still an applicant ( }
}
me.state.lastCircleStatus = me.circle.rawStatus;
[me.state writeToStorage];
}];
[me scheduleActivityAt:me.state.pendingApplicationReminder];
}
-(BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification
{
return YES;
}
-(void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
{
if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
[self notifyiCloudPreferencesAbout:notification.userInfo[@"Activate"]];
}
// The "Later" seems handled Ok without doing anything here, but KickedOut & other special items need an action
if (notification.userInfo[@"SPECIAL"]) {
NSLog(@"ACTIVATED (remove): [appropriateNotificationCenter() removeDeliveredNotification:notification];
} else {
NSLog(@"ACTIVATED (NOT removed): }
}
-(void)userNotificationCenter:(NSUserNotificationCenter *)center didDismissAlert:(NSUserNotification *)notification
{
[self notifyiCloudPreferencesAbout:notification.userInfo[@"Dismiss"]];
if (!notification.userInfo[@"SPECIAL"]) {
// If we don't do anything here & another notification comes in we
// will repost the alert, which will be dumb.
id applicantId = notification.userInfo[@"applicantId"];
if (applicantId != nil) {
[self.viewedIds addObject:applicantId];
}
NSLog(@"DISMISS (t) } else {
NSLog(@"DISMISS (f) [appropriateNotificationCenter() removeDeliveredNotification:notification];
}
}
-(void)postForApplicant:(KDCirclePeer*)applicant
{
static int postCount = 0;
if ([self.viewedIds containsObject:applicant.idString]) {
NSLog(@"Already viewed return;
}
NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
for (NSUserNotification *note in noteCenter.deliveredNotifications) {
if ([applicant.idString isEqualToString:note.userInfo[@"applicantId"]]) {
if (note.isPresented) {
NSLog(@"Already posted&presented: return;
} else {
NSLog(@"Already posted, but not presented: }
}
}
NSUserNotification *note = [NSUserNotification new];
// Genstrings command line is: genstrings -o en.lproj -u KNAppDelegate.m
note.title = [NSString stringWithFormat:NSLocalizedString(@"iCloud Keychain", @"Title for new keychain syncing device notification")];
note.informativeText = [NSString stringWithFormat:NSLocalizedString(@"\\U201C
note.hasActionButton = YES;
note._displayStyle = _NSUserNotificationDisplayStyleAlert;
note._identityImage = [NSImage bundleImage];
note._identityImageHasBorder = NO;
note._actionButtonIsSnooze = YES;
note.actionButtonTitle = NSLocalizedString(@"Later", @"Button label to dismiss device notification");
note.otherButtonTitle = NSLocalizedString(@"View", @"Button label to view device notification");
note.identifier = [[NSUUID new] UUIDString];
note.userInfo = @{@"applicantName": applicant.name,
@"applicantId": applicant.idString,
@"Dismiss": (__bridge NSString *)kMMPropertyKeychainAADetailsAEAction,
};
NSLog(@"About to post# [appropriateNotificationCenter() deliverNotification:note];
postCount++;
}
-(void)postKickedOutWithMessage:(NSString*)body
{
NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
for (NSUserNotification *note in noteCenter.deliveredNotifications) {
if (note.userInfo[@"KickedOut"]) {
if (note.isPresented) {
NSLog(@"Already posted&presented (removing): [appropriateNotificationCenter() removeDeliveredNotification: note];
} else {
NSLog(@"Already posted, but not presented: }
}
}
NSUserNotification *note = [NSUserNotification new];
note.title = NSLocalizedString(@"iCloud Keychain Was Reset", @"Title for iCloud Keychain Reset notification");
note.informativeText = body; // Already LOCed
note._identityImage = [NSImage bundleImage];
note._identityImageHasBorder = NO;
note.otherButtonTitle = NSLocalizedString(@"Close", @"Close button");
note.actionButtonTitle = NSLocalizedString(@"Options", @"Options Button");
note.identifier = [[NSUUID new] UUIDString];
note.userInfo = @{@"KickedOut": @1,
@"SPECIAL": @1,
@"Activate": (__bridge NSString *)kMMPropertyKeychainMRDetailsAEAction,
};
NSLog(@"About to post#-/ [appropriateNotificationCenter() deliverNotification:note];
}
-(void)postApplicationReminder
{
NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
for (NSUserNotification *note in noteCenter.deliveredNotifications) {
if (note.userInfo[@"ApplicationReminder"]) {
if (note.isPresented) {
NSLog(@"Already posted&presented (removing): [appropriateNotificationCenter() removeDeliveredNotification: note];
} else {
NSLog(@"Already posted, but not presented: }
}
}
NSUserNotification *note = [NSUserNotification new];
note.title = NSLocalizedString(@"iCloud Keychain", @"Title for iCloud Keychain Application still pending (from this device) reminder");
note.informativeText = NSLocalizedString(@"Approve this Mac from another device to use iCloud Keychain.", @"Body text for iCloud Keychain Application still pending (from this device) reminder");
note._identityImage = [NSImage bundleImage];
note._identityImageHasBorder = NO;
note.otherButtonTitle = NSLocalizedString(@"Close", @"Close button");
note.actionButtonTitle = NSLocalizedString(@"Options", @"Options Button");
note.identifier = [[NSUUID new] UUIDString];
note.userInfo = @{@"ApplicationReminder": @1,
@"SPECIAL": @1,
@"Activate": (__bridge NSString *)kMMPropertyKeychainWADetailsAEAction,
};
NSLog(@"About to post#-/ [appropriateNotificationCenter() deliverNotification:note];
}
@end