//
// AppController.m
// SelfControl
//
// Created by Charlie Stigler on 1/29/09.
// Copyright 2009 Eyebeam.
// This file is part of SelfControl.
//
// SelfControl is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
#import "AppController.h"
#import "MASPreferencesWindowController.h"
#import "PreferencesGeneralViewController.h"
#import "PreferencesAdvancedViewController.h"
#import "SCTimeIntervalFormatter.h"
#import
#import
#import "SCSettings.h"
#import
#import "SCXPCClient.h"
#import "HostFileBlocker.h"
#import "SCBlockFileReaderWriter.h"
@interface AppController () {}
@property (atomic, strong, readwrite) SCXPCClient* xpc;
@end
@implementation AppController {
NSWindowController* getStartedWindowController;
}
@synthesize addingBlock;
- (AppController*) init {
if(self = [super init]) {
defaults_ = [NSUserDefaults standardUserDefaults];
[defaults_ registerDefaults: SCConstants.defaultUserDefaults];
self.addingBlock = false;
// refreshUILock_ is a lock that prevents a race condition by making the refreshUserInterface
// method alter the blockIsOn variable atomically (will no longer be necessary once we can
// use properties).
refreshUILock_ = [[NSLock alloc] init];
}
return self;
}
- (IBAction)updateTimeSliderDisplay:(id)sender {
NSInteger numMinutes = [defaults_ integerForKey: @"BlockDuration"];
// if the duration is larger than we can display on our slider
// chop it down to our max display value so the user doesn't
// accidentally start a much longer block than intended
if (numMinutes > blockDurationSlider_.maxValue) {
[self setDefaultsBlockDurationOnMainThread: @(floor(blockDurationSlider_.maxValue))];
numMinutes = [defaults_ integerForKey: @"BlockDuration"];
}
// Time-display code cleaned up thanks to the contributions of many users
NSString* timeString = [self timeSliderDisplayStringFromNumberOfMinutes:numMinutes];
[blockSliderTimeDisplayLabel_ setStringValue:timeString];
[submitButton_ setEnabled: (numMinutes > 0) && ([[defaults_ arrayForKey: @"Blocklist"] count] > 0)];
}
- (NSString *)timeSliderDisplayStringFromNumberOfMinutes:(NSInteger)numberOfMinutes {
static NSCalendar* gregorian = nil;
if (gregorian == nil) {
gregorian = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
}
NSRange secondsRangePerMinute = [gregorian
rangeOfUnit:NSCalendarUnitSecond
inUnit:NSCalendarUnitMinute
forDate:[NSDate date]];
NSUInteger numberOfSecondsPerMinute = NSMaxRange(secondsRangePerMinute);
NSTimeInterval numberOfSecondsSelected = (NSTimeInterval)(numberOfSecondsPerMinute * numberOfMinutes);
NSString* displayString = [self timeSliderDisplayStringFromTimeInterval:numberOfSecondsSelected];
return displayString;
}
- (NSString *)timeSliderDisplayStringFromTimeInterval:(NSTimeInterval)numberOfSeconds {
static SCTimeIntervalFormatter* formatter = nil;
if (formatter == nil) {
formatter = [[SCTimeIntervalFormatter alloc] init];
}
NSString* formatted = [formatter stringForObjectValue:@(numberOfSeconds)];
return formatted;
}
- (IBAction)addBlock:(id)sender {
if ([self blockIsRunning]) {
// This method shouldn't be getting called, a block is on so the Start button should be disabled.
NSError* err = [SCErr errorWithCode: 104];
[SCSentry captureError: err];
[NSApp presentError: err];
return;
}
if([[defaults_ arrayForKey: @"Blocklist"] count] == 0) {
// Since the Start button should be disabled when the blocklist has no entries,
// this should definitely not be happening. Exit.
NSError* err = [SCErr errorWithCode: 101];
[SCSentry captureError: err];
[NSApp presentError: err];
return;
}
if([defaults_ boolForKey: @"VerifyInternetConnection"] && ![self networkConnectionIsAvailable]) {
NSAlert* networkUnavailableAlert = [[NSAlert alloc] init];
[networkUnavailableAlert setMessageText: NSLocalizedString(@"No network connection detected", "No network connection detected message")];
[networkUnavailableAlert setInformativeText:NSLocalizedString(@"A block cannot be started without a working network connection. You can override this setting in Preferences.", @"Message when network connection is unavailable")];
[networkUnavailableAlert addButtonWithTitle: NSLocalizedString(@"Cancel", "Cancel button")];
[networkUnavailableAlert addButtonWithTitle: NSLocalizedString(@"Network Diagnostics...", @"Network Diagnostics button")];
if([networkUnavailableAlert runModal] == NSAlertFirstButtonReturn)
return;
// If the user selected Network Diagnostics launch an assisant to help them.
// apple.com is an arbitrary host chosen to pass to Network Diagnostics.
CFURLRef url = CFURLCreateWithString(NULL, CFSTR("http://apple.com"), NULL);
CFNetDiagnosticRef diagRef = CFNetDiagnosticCreateWithURL(kCFAllocatorDefault, url);
CFNetDiagnosticDiagnoseProblemInteractively(diagRef);
return;
}
[timerWindowController_ resetStrikes];
[NSThread detachNewThreadSelector: @selector(installBlock) toTarget: self withObject: nil];
}
- (void)refreshUserInterface {
// UI updates are for the main thread only!
if (![NSThread isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self refreshUserInterface];
});
return;
}
if(![refreshUILock_ tryLock]) {
// already refreshing the UI, no need to wait and do it again
return;
}
BOOL blockWasOn = blockIsOn;
blockIsOn = [self blockIsRunning];
if(blockIsOn) { // block is on
if(!blockWasOn) { // if we just switched states to on...
[self closeTimerWindow];
[self showTimerWindow];
[initialWindow_ close];
[self closeDomainList];
}
} else { // block is off
if(blockWasOn) { // if we just switched states to off...
[timerWindowController_ blockEnded];
// Makes sure the domain list will refresh when it comes back
[self closeDomainList];
NSWindow* mainWindow = [NSApp mainWindow];
// We don't necessarily want the initial window to be key and front,
// but no other message seems to show it properly.
[initialWindow_ makeKeyAndOrderFront: self];
// So we work around it and make key and front whatever was the main window
[mainWindow makeKeyAndOrderFront: self];
// make sure the dock badge is cleared
[[NSApp dockTile] setBadgeLabel: nil];
[self closeTimerWindow];
}
[self updateTimeSliderDisplay: blockDurationSlider_];
if([defaults_ integerForKey: @"BlockDuration"] != 0 && [[defaults_ arrayForKey: @"Blocklist"] count] != 0 && !self.addingBlock) {
[submitButton_ setEnabled: YES];
} else {
[submitButton_ setEnabled: NO];
}
// If we're adding a block, we want buttons disabled.
if(!self.addingBlock) {
[blockDurationSlider_ setEnabled: YES];
[editBlocklistButton_ setEnabled: YES];
[submitButton_ setTitle: NSLocalizedString(@"Start", @"Start button")];
} else {
[blockDurationSlider_ setEnabled: NO];
[editBlocklistButton_ setEnabled: NO];
[submitButton_ setTitle: NSLocalizedString(@"Loading", @"Loading button")];
}
// if block's off, and we haven't shown it yet, show the first-time modal
if (![defaults_ boolForKey: @"GetStartedShown"]) {
[defaults_ setBool: YES forKey: @"GetStartedShown"];
[self showGetStartedWindow: self];
}
}
// finally: if the helper tool marked that it detected tampering, make sure
// we follow through and set the cheater wallpaper (helper tool can't do it itself)
if ([settings_ boolForKey: @"TamperingDetected"]) {
NSURL* cheaterBackgroundURL = [[NSBundle mainBundle] URLForResource: @"cheater-background" withExtension: @"png"];
NSArray* screens = [NSScreen screens];
for (NSScreen* screen in screens) {
NSError* err;
[[NSWorkspace sharedWorkspace] setDesktopImageURL: cheaterBackgroundURL
forScreen: screen
options: @{}
error: &err];
}
[settings_ setValue: @NO forKey: @"TamperingDetected"];
}
// Display "blocklist" or "allowlist" as appropriate
NSString* listType = [defaults_ boolForKey: @"BlockAsWhitelist"] ? @"Allowlist" : @"Blocklist";
NSString* editListString = NSLocalizedString(([NSString stringWithFormat: @"Edit %@", listType]), @"Edit list button / menu item");
editBlocklistButton_.title = editListString;
editBlocklistMenuItem_.title = editListString;
[refreshUILock_ unlock];
}
- (void)handleConfigurationChangedNotification {
[SCSentry addBreadcrumb: @"Received configuration changed notification" category: @"app"];
// if our configuration changed, we should assume the settings may have changed
[[SCSettings sharedSettings] reloadSettings];
// and our interface may need to change to match!
[self refreshUserInterface];
}
- (void)showTimerWindow {
if(timerWindowController_ == nil) {
[[NSBundle mainBundle] loadNibNamed: @"TimerWindow" owner: self topLevelObjects: nil];
} else {
[[timerWindowController_ window] makeKeyAndOrderFront: self];
[[timerWindowController_ window] center];
}
}
- (void)closeTimerWindow {
[timerWindowController_ close];
timerWindowController_ = nil;
}
- (IBAction)openPreferences:(id)sender {
[SCSentry addBreadcrumb: @"Opening preferences window" category: @"app"];
if (preferencesWindowController_ == nil) {
NSViewController* generalViewController = [[PreferencesGeneralViewController alloc] init];
NSViewController* advancedViewController = [[PreferencesAdvancedViewController alloc] init];
NSString* title = NSLocalizedString(@"Preferences", @"Common title for Preferences window");
preferencesWindowController_ = [[MASPreferencesWindowController alloc] initWithViewControllers: @[generalViewController, advancedViewController] title: title];
}
[preferencesWindowController_ showWindow: nil];
}
- (IBAction)showGetStartedWindow:(id)sender {
[SCSentry addBreadcrumb: @"Showing \"Get Started\" window" category: @"app"];
if (!getStartedWindowController) {
getStartedWindowController = [[NSWindowController alloc] initWithWindowNibName: @"FirstTime"];
}
[getStartedWindowController.window center];
[getStartedWindowController.window makeKeyAndOrderFront: nil];
[getStartedWindowController showWindow: nil];
}
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
// For test runs, we don't want to pop up the dialog to move to the Applications folder, as it breaks the tests
if (NSProcessInfo.processInfo.environment[@"XCTestConfigurationFilePath"] == nil) {
PFMoveToApplicationsFolderIfNecessary();
}
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApplication sharedApplication].delegate = self;
[SCSentry startSentry: @"org.eyebeam.SelfControl"];
settings_ = [SCSettings sharedSettings];
// go copy over any preferences from legacy setting locations
// (we won't clear any old data yet - we leave that to the daemon)
if ([SCMigrationUtilities legacySettingsFoundForCurrentUser]) {
[SCMigrationUtilities copyLegacySettingsToDefaults];
}
// start up our daemon XPC
self.xpc = [SCXPCClient new];
[self.xpc connectToHelperTool];
// if we don't have a connection within 0.5 seconds,
// OR we get back a connection with an old daemon version
// AND we're running a modern block (which should have a daemon running it)
// something's wrong with our app-daemon connection. This probably means one of two things:
// 1. The daemon got unloaded somehow and failed to restart. This is a big problem because the block won't come off.
// 2. The daemon doesn't want to talk to us anymore, potentially because we've changed our signing certificate. This is a
// smaller problem, but still not great because the app can't communicate anything to the daemon.
// 3. There's a daemon but it's an old version, and should be replaced.
// in any case, let's go try to reinstall the daemon
// (we debounce this call so it happens only once, after the connection has been invalidated for an extended period)
if ([SCBlockUtilities modernBlockIsRunning]) {
[NSTimer scheduledTimerWithTimeInterval: 0.5 repeats: NO block:^(NSTimer * _Nonnull timer) {
[self.xpc getVersion:^(NSString * _Nonnull daemonVersion, NSError * _Nonnull error) {
if (error == nil) {
if ([SELFCONTROL_VERSION_STRING compare: daemonVersion options: NSNumericSearch] == NSOrderedDescending) {
NSLog(@"Daemon version of %@ is out of date (current version is %@).", daemonVersion, SELFCONTROL_VERSION_STRING);
[SCSentry addBreadcrumb: @"Detected out-of-date daemon" category: @"app"];
[self reinstallDaemon];
} else {
[SCSentry addBreadcrumb: @"Detected up-to-date daemon" category:@"app"];
NSLog(@"Daemon version of %@ is up-to-date!", daemonVersion);
}
} else {
NSLog(@"ERROR: Fetching daemon version failed with error %@", error);
[self reinstallDaemon];
}
}];
}];
}
// Register observers on both distributed and normal notification centers
// to receive notifications from the helper tool and the other parts of the
// main SelfControl app. Note that they are divided thusly because distributed
// notifications are very expensive and should be minimized.
[[NSDistributedNotificationCenter defaultCenter] addObserver: self
selector: @selector(handleConfigurationChangedNotification)
name: @"SCConfigurationChangedNotification"
object: nil
suspensionBehavior: NSNotificationSuspensionBehaviorDeliverImmediately];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(handleConfigurationChangedNotification)
name: @"SCConfigurationChangedNotification"
object: nil];
[initialWindow_ center];
// We'll set blockIsOn to whatever is NOT right, so that in refreshUserInterface
// it'll fix it and properly refresh the user interface.
blockIsOn = ![self blockIsRunning];
// Change block duration slider for hidden user defaults settings
long numTickMarks = ([defaults_ integerForKey: @"MaxBlockLength"] / [defaults_ integerForKey: @"BlockLengthInterval"]) + 1;
[blockDurationSlider_ setMaxValue: [defaults_ integerForKey: @"MaxBlockLength"]];
[blockDurationSlider_ setNumberOfTickMarks: numTickMarks];
[blockDurationSlider_ bind: @"value"
toObject: [NSUserDefaultsController sharedUserDefaultsController]
withKeyPath: @"values.BlockDuration"
options: @{
NSContinuouslyUpdatesValueBindingOption: @YES
}];
[self refreshUserInterface];
NSOperatingSystemVersion minRequiredVersion = (NSOperatingSystemVersion){10,10,0}; // Mountain Lion
NSString* minRequiredVersionString = @"10.10 (Yosemite)";
if (![[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: minRequiredVersion]) {
NSLog(@"ERROR: Unsupported version for SelfControl");
[SCSentry captureMessage: @"Unsupported operating system version"];
NSAlert* unsupportedVersionAlert = [[NSAlert alloc] init];
[unsupportedVersionAlert setMessageText: NSLocalizedString(@"Unsupported version", nil)];
[unsupportedVersionAlert setInformativeText: [NSString stringWithFormat: NSLocalizedString(@"This version of SelfControl only supports Mac OS X version %@ or higher. To download a version for older operating systems, please go to www.selfcontrolapp.com", nil), minRequiredVersionString]];
[unsupportedVersionAlert addButtonWithTitle: NSLocalizedString(@"OK", nil)];
[unsupportedVersionAlert runModal];
}
}
- (void)applicationWillTerminate:(NSNotification *)notification {
[settings_ synchronizeSettings];
}
- (void)reinstallDaemon {
NSLog(@"Attempting to reinstall daemon...");
[SCSentry addBreadcrumb: @"Reinstalling daemon" category:@"app"];
[self.xpc installDaemon:^(NSError * _Nonnull error) {
if (error == nil) {
NSLog(@"Reinstalled daemon successfully!");
[SCSentry addBreadcrumb: @"Daemon reinstalled successfully" category:@"app"];
NSLog(@"Retrying helper tool connection...");
[self.xpc performSelectorOnMainThread: @selector(connectToHelperTool) withObject: nil waitUntilDone: YES];
} else {
if (![SCMiscUtilities errorIsAuthCanceled: error]) {
NSLog(@"ERROR: Reinstalling daemon failed with error %@", error);
[NSApp presentError: error];
}
}
}];
}
- (BOOL)blockIsRunning {
// we'll say a block is running if we find the block info, but
// also, importantly, if we find a block still going in the hosts file
// that way if this happens, the user will still see the timer window -
// which will let them manually clear the remaining block info after 10 seconds
return [SCBlockUtilities anyBlockIsRunning] || [HostFileBlocker blockFoundInHostsFile];
}
- (IBAction)showDomainList:(id)sender {
[SCSentry addBreadcrumb: @"Showing domain list" category:@"app"];
if([self blockIsRunning] || self.addingBlock) {
NSAlert* blockInProgressAlert = [[NSAlert alloc] init];
[blockInProgressAlert setMessageText: NSLocalizedString(@"Block in progress", @"Block in progress error title")];
[blockInProgressAlert setInformativeText:NSLocalizedString(@"The blocklist cannot be edited while a block is in progress.", @"Block in progress explanation")];
[blockInProgressAlert addButtonWithTitle: NSLocalizedString(@"OK", @"OK button")];
[blockInProgressAlert runModal];
return;
}
if(domainListWindowController_ == nil) {
[[NSBundle mainBundle] loadNibNamed: @"DomainList" owner: self topLevelObjects: nil];
}
[domainListWindowController_ showWindow: self];
}
- (void)closeDomainList {
[domainListWindowController_ close];
domainListWindowController_ = nil;
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed: (NSApplication*) theApplication {
// Hack to make the application terminate after the last window is closed, but
// INCLUDE the HUD-style timer window.
if([[timerWindowController_ window] isVisible])
return NO;
if (PFMoveIsInProgress())
return NO;
return YES;
}
- (BOOL)networkConnectionIsAvailable {
SCNetworkReachabilityFlags flags;
// This method goes haywire if Google ever goes down...
SCNetworkReachabilityRef target = SCNetworkReachabilityCreateWithName (kCFAllocatorDefault, "google.com");
BOOL reachable = SCNetworkReachabilityGetFlags (target, &flags);
return reachable && (flags & kSCNetworkFlagsReachable) && !(flags & kSCNetworkFlagsConnectionRequired);
}
- (void)addToBlockList:(NSString*)host lock:(NSLock*)lock {
NSLog(@"addToBlocklist: %@", host);
// Note we RETRIEVE the latest list from settings (ActiveBlocklist), but we SET the new list in defaults
// since the helper daemon should be the only one changing ActiveBlocklist
NSMutableArray* list = [[settings_ valueForKey: @"ActiveBlocklist"] mutableCopy];
NSArray* cleanedEntries = [SCMiscUtilities cleanBlocklistEntry: host];
if (cleanedEntries.count == 0) return;
for (int i = 0; i < cleanedEntries.count; i++) {
NSString* entry = cleanedEntries[i];
[list addObject: entry];
}
[defaults_ setValue: list forKey: @"Blocklist"];
if(![self blockIsRunning]) {
// This method shouldn't be getting called, a block is not on.
// so the Start button should be disabled.
// Maybe the UI didn't get properly refreshed, so try refreshing it again
// before we return.
[self refreshUserInterface];
NSError* err = [SCErr errorWithCode: 102];
[SCSentry captureError: err];
[NSApp presentError: err];
return;
}
if([defaults_ boolForKey: @"VerifyInternetConnection"] && ![self networkConnectionIsAvailable]) {
NSAlert* networkUnavailableAlert = [[NSAlert alloc] init];
[networkUnavailableAlert setMessageText: NSLocalizedString(@"No network connection detected", "No network connection detected message")];
[networkUnavailableAlert setInformativeText:NSLocalizedString(@"A block cannot be started without a working network connection. You can override this setting in Preferences.", @"Message when network connection is unavailable")];
[networkUnavailableAlert addButtonWithTitle: NSLocalizedString(@"Cancel", "Cancel button")];
[networkUnavailableAlert addButtonWithTitle: NSLocalizedString(@"Network Diagnostics...", @"Network Diagnostics button")];
if([networkUnavailableAlert runModal] == NSAlertFirstButtonReturn) {
// User clicked cancel
return;
}
// If the user selected Network Diagnostics, launch an assisant to help them.
// apple.com is an arbitrary host chosen to pass to Network Diagnostics.
CFURLRef url = CFURLCreateWithString(NULL, CFSTR("http://apple.com"), NULL);
CFNetDiagnosticRef diagRef = CFNetDiagnosticCreateWithURL(kCFAllocatorDefault, url);
CFNetDiagnosticDiagnoseProblemInteractively(diagRef);
return;
}
[NSThread detachNewThreadSelector: @selector(updateActiveBlocklist:) toTarget: self withObject: lock];
}
- (void)extendBlockTime:(NSInteger)minutesToAdd lock:(NSLock*)lock {
// sanity check: extending a block for 0 minutes is useless; 24 hour should be impossible
NSInteger maxBlockLength = [defaults_ integerForKey: @"MaxBlockLength"];
if(minutesToAdd < 1) return;
if (minutesToAdd > maxBlockLength) {
minutesToAdd = maxBlockLength;
}
// ensure block health before we try to change it
if(![self blockIsRunning]) {
// This method shouldn't be getting called, a block is not on.
// so the Start button should be disabled.
// Maybe the UI didn't get properly refreshed, so try refreshing it again
// before we return.
[self refreshUserInterface];
NSError* err = [SCErr errorWithCode: 103];
[SCSentry captureError: err];
[NSApp presentError: err];
return;
}
[self updateBlockEndDate: lock minutesToAdd: minutesToAdd];
// [NSThread detachNewThreadSelector: @selector(extendBlockDuration:)
// toTarget: self
// withObject: @{
// @"lock": lock,
// @"minutesToAdd": @(minutesToAdd)
// }];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver: self
name: @"SCConfigurationChangedNotification"
object: nil];
[[NSDistributedNotificationCenter defaultCenter] removeObserver: self
name: @"SCConfigurationChangedNotification"
object: nil];
}
- (id)initialWindow {
return initialWindow_;
}
- (id)domainListWindowController {
return domainListWindowController_;
}
- (void)setDomainListWindowController:(id)newController {
domainListWindowController_ = newController;
}
- (void)installBlock {
[SCSentry addBreadcrumb: @"App running installBlock method" category:@"app"];
@autoreleasepool {
self.addingBlock = true;
[self refreshUserInterface];
[self.xpc installDaemon:^(NSError * _Nonnull error) {
if (error != nil) {
if (![SCMiscUtilities errorIsAuthCanceled: error]) {
[NSApp performSelectorOnMainThread: @selector(presentError:)
withObject: error
waitUntilDone: YES];
}
self.addingBlock = false;
[self refreshUserInterface];
return;
} else {
[SCSentry addBreadcrumb: @"Daemon installed successfully (en route to installing block)" category:@"app"];
// helper tool installed successfully, let's prepare to start the block!
// for legacy reasons, BlockDuration is in minutes, so convert it to seconds before passing it through]
// sanity check duration (must be above zero)
NSTimeInterval blockDurationSecs = MAX([[self->defaults_ valueForKey: @"BlockDuration"] intValue] * 60, 0);
NSDate* newBlockEndDate = [NSDate dateWithTimeIntervalSinceNow: blockDurationSecs];
// we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid
[self->settings_ synchronizeSettings];
[self->defaults_ synchronize];
// ok, the new helper tool is installed! refresh the connection, then it's time to start the block
[self.xpc refreshConnectionAndRun:^{
NSLog(@"Refreshed connection and ready to start block!");
[self.xpc startBlockWithControllingUID: getuid()
blocklist: [self->defaults_ arrayForKey: @"Blocklist"]
isAllowlist: [self->defaults_ boolForKey: @"BlockAsWhitelist"]
endDate: newBlockEndDate
blockSettings: @{
@"ClearCaches": [self->defaults_ valueForKey: @"ClearCaches"],
@"AllowLocalNetworks": [self->defaults_ valueForKey: @"AllowLocalNetworks"],
@"EvaluateCommonSubdomains": [self->defaults_ valueForKey: @"EvaluateCommonSubdomains"],
@"IncludeLinkedDomains": [self->defaults_ valueForKey: @"IncludeLinkedDomains"],
@"BlockSoundShouldPlay": [self->defaults_ valueForKey: @"BlockSoundShouldPlay"],
@"BlockSound": [self->defaults_ valueForKey: @"BlockSound"],
@"EnableErrorReporting": [self->defaults_ valueForKey: @"EnableErrorReporting"]
}
reply:^(NSError * _Nonnull error) {
if (error != nil ) {
if (![SCMiscUtilities errorIsAuthCanceled: error]) {
[NSApp performSelectorOnMainThread: @selector(presentError:)
withObject: error
waitUntilDone: YES];
}
} else {
[SCSentry addBreadcrumb: @"Block started successfully" category:@"app"];
}
// get the new settings
[self->settings_ synchronizeSettingsWithCompletion:^(NSError * _Nullable error) {
self.addingBlock = false;
[self refreshUserInterface];
}];
}];
}];
}
}];
}
}
- (void)updateActiveBlocklist:(NSLock*)lockToUse {
if(![lockToUse tryLock]) {
return;
}
[SCSentry addBreadcrumb: @"App running updateActiveBlocklist method" category:@"app"];
// we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid
[settings_ synchronizeSettings];
[defaults_ synchronize];
[self.xpc refreshConnectionAndRun:^{
NSLog(@"Refreshed connection updating active blocklist!");
[self.xpc updateBlocklist: [self->defaults_ arrayForKey: @"Blocklist"]
reply:^(NSError * _Nonnull error) {
[self->timerWindowController_ performSelectorOnMainThread:@selector(closeAddSheet:) withObject: self waitUntilDone: YES];
if (error != nil) {
if (![SCMiscUtilities errorIsAuthCanceled: error]) {
[NSApp performSelectorOnMainThread: @selector(presentError:)
withObject: error
waitUntilDone: YES];
}
} else {
[SCSentry addBreadcrumb: @"Blocklist updated successfully" category:@"app"];
}
[lockToUse unlock];
}];
}];
}
// it really sucks, but we can't change any values that are KVO-bound to the UI unless they're on the main thread
// to make that easier, here is a helper that always does it on the main thread
- (void)setDefaultsBlockDurationOnMainThread:(NSNumber*)newBlockDuration {
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread: @selector(setDefaultsBlockDurationOnMainThread:) withObject:newBlockDuration waitUntilDone: YES];
}
[defaults_ setInteger: [newBlockDuration intValue] forKey: @"BlockDuration"];
}
- (void)updateBlockEndDate:(NSLock*)lockToUse minutesToAdd:(NSInteger)minutesToAdd {
if(![lockToUse tryLock]) {
return;
}
[SCSentry addBreadcrumb: @"App running updateBlockEndDate method" category:@"app"];
minutesToAdd = MAX(minutesToAdd, 0); // make sure there's no funny business with negative minutes
NSDate* oldBlockEndDate = [settings_ valueForKey: @"BlockEndDate"];
NSDate* newBlockEndDate = [oldBlockEndDate dateByAddingTimeInterval: (minutesToAdd * 60)];
// we're about to launch a helper tool which will read settings, so make sure the ones on disk are valid
[settings_ synchronizeSettings];
[defaults_ synchronize];
[self.xpc refreshConnectionAndRun:^{
// Before we try to extend the block, make sure the block time didn't run out (or is about to run out) in the meantime
if ([SCBlockUtilities currentBlockIsExpired] || [oldBlockEndDate timeIntervalSinceNow] < 1) {
// we're done, or will be by the time we get to it! so just let it expire. they can restart it.
[lockToUse unlock];
return;
}
NSLog(@"Refreshed connection updating active block end date!");
[self.xpc updateBlockEndDate: newBlockEndDate
reply:^(NSError * _Nonnull error) {
[self->timerWindowController_ performSelectorOnMainThread:@selector(closeAddSheet:) withObject: self waitUntilDone: YES];
// let the timer know it needs to recalculate
[self->timerWindowController_ performSelectorOnMainThread:@selector(blockEndDateUpdated)
withObject: nil
waitUntilDone: YES];
if (error != nil) {
if (![SCMiscUtilities errorIsAuthCanceled: error]) {
[NSApp performSelectorOnMainThread: @selector(presentError:)
withObject: error
waitUntilDone: YES];
}
} else {
[SCSentry addBreadcrumb: @"App extended block duration successfully" category:@"app"];
}
[lockToUse unlock];
}];
}];
}
- (IBAction)save:(id)sender {
NSSavePanel *sp;
long runResult;
/* create or get the shared instance of NSSavePanel */
sp = [NSSavePanel savePanel];
sp.allowedFileTypes = @[@"selfcontrol"];
/* display the NSSavePanel */
runResult = [sp runModal];
/* if successful, save file under designated name */
if (runResult == NSModalResponseOK) {
NSError* err;
[SCBlockFileReaderWriter writeBlocklistToFileURL: sp.URL
blockInfo: @{
@"Blocklist": [defaults_ arrayForKey: @"Blocklist"],
@"BlockAsWhitelist": [defaults_ objectForKey: @"BlockAsWhitelist"]
}
error: &err];
if (err != nil) {
NSError* displayErr = [SCErr errorWithCode: 105 subDescription: err.localizedDescription];
[SCSentry captureError: displayErr];
NSBeep();
[NSApp presentError: displayErr];
return;
} else {
[SCSentry addBreadcrumb: @"Saved blocklist to file" category:@"app"];
}
}
}
- (BOOL)openSavedBlockFileAtURL:(NSURL*)fileURL {
NSDictionary* settingsFromFile = [SCBlockFileReaderWriter readBlocklistFromFile: fileURL];
if (settingsFromFile != nil) {
[defaults_ setObject: settingsFromFile[@"Blocklist"] forKey: @"Blocklist"];
[defaults_ setObject: settingsFromFile[@"BlockAsWhitelist"] forKey: @"BlockAsWhitelist"];
[SCSentry addBreadcrumb: @"Opened blocklist from file" category:@"app"];
} else {
NSLog(@"WARNING: Could not read a valid blocklist from file - ignoring.");
return NO;
}
// close the domain list (and reopen again if need be to refresh)
BOOL domainListIsOpen = [[domainListWindowController_ window] isVisible];
NSRect frame = [[domainListWindowController_ window] frame];
[self closeDomainList];
if(domainListIsOpen) {
[self showDomainList: self];
[[domainListWindowController_ window] setFrame: frame display: YES];
}
[self refreshUserInterface];
return YES;
}
- (IBAction)open:(id)sender {
NSOpenPanel* oPanel = [NSOpenPanel openPanel];
oPanel.allowedFileTypes = @[@"selfcontrol"];
oPanel.allowsMultipleSelection = NO;
long result = [oPanel runModal];
if (result == NSModalResponseOK) {
if([oPanel.URLs count] > 0) {
[self openSavedBlockFileAtURL: oPanel.URLs[0]];
}
}
}
- (BOOL)application:(NSApplication*)theApplication openFile:(NSString*)filename {
return [self openSavedBlockFileAtURL: [NSURL fileURLWithPath: filename]];
}
- (IBAction)openFAQ:(id)sender {
[SCSentry addBreadcrumb: @"Opened SelfControl FAQ" category:@"app"];
NSURL *url=[NSURL URLWithString: @"https://github.com/SelfControlApp/selfcontrol/wiki/FAQ#q-selfcontrols-timer-is-at-0000-and-i-cant-start-a-new-block-and-im-freaking-out"];
[[NSWorkspace sharedWorkspace] openURL: url];
}
@end