source: trunk/Cocoa/Pester/Source/PSAlarm.m@ 111

Last change on this file since 111 was 105, checked in by Nicholas Riley, 21 years ago

PSAlarm.m: Fixed logic bug with invalid date/time combination. Added
repeating indicator to -description. Fixed plist archiving to
properly handle repeating alarms which expire while the app is quit
(1).

PSAlarmAlertController.m: Restore some debugging.

PSPowerManager.m: Properly return pmuReference (fixes compiler
warning, and 2).

Read Me.rtfd: Fixed some wording.

File size: 19.1 KB
RevLine 
[24]1//
2// PSAlarm.m
3// Pester
4//
5// Created by Nicholas Riley on Wed Oct 09 2002.
6// Copyright (c) 2002 Nicholas Riley. All rights reserved.
7//
8
9#import "PSAlarm.h"
[34]10#import "PSAlert.h"
[53]11#import "PSAlerts.h"
[61]12#import "PSTimer.h"
[43]13#import "NJRDateFormatter.h"
[53]14#import "NSCalendarDate-NJRExtensions.h"
15#import "NSDictionary-NJRExtensions.h"
16#import "NSString-NJRExtensions.h"
[24]17
[26]18NSString * const PSAlarmTimerSetNotification = @"PSAlarmTimerSetNotification";
19NSString * const PSAlarmTimerExpiredNotification = @"PSAlarmTimerExpiredNotification";
[53]20NSString * const PSAlarmDiedNotification = @"PSAlarmDiedNotification";
[24]21
[53]22// property list keys
23static NSString * const PLAlarmType = @"type"; // NSString
24static NSString * const PLAlarmDate = @"date"; // NSNumber
25static NSString * const PLAlarmInterval = @"interval"; // NSNumber
26static NSString * const PLAlarmSnoozeInterval = @"snooze interval"; // NSNumber
27static NSString * const PLAlarmMessage = @"message"; // NSString
28static NSString * const PLAlarmAlerts = @"alerts"; // NSDictionary
29static NSString * const PLAlarmRepeating = @"repeating"; // NSNumber
30
[43]31static NSString *dateFormat, *shortDateFormat, *timeFormat;
32static NSDictionary *locale;
33
[34]34// XXX need to reset pending alarms after sleep, they "freeze" and never expire.
35
[24]36@implementation PSAlarm
37
[51]38#pragma mark initialize-release
39
[43]40+ (void)initialize; // XXX change on locale modification, subscribe to NSNotifications
41{
42 dateFormat = [[NJRDateFormatter localizedDateFormatIncludingWeekday: YES] retain];
43 shortDateFormat = [[NJRDateFormatter localizedShortDateFormatIncludingWeekday: NO] retain];
44 timeFormat = [[NJRDateFormatter localizedTimeFormatIncludingSeconds: YES] retain];
45 locale = [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] retain];
46}
47
[26]48- (void)dealloc;
49{
50 // NSLog(@"DEALLOC %@", self);
51 alarmType = PSAlarmInvalid;
52 [alarmDate release]; alarmDate = nil;
53 [alarmMessage release]; alarmMessage = nil;
54 [invalidMessage release]; invalidMessage = nil;
[28]55 [timer invalidate]; [timer release]; timer = nil;
[34]56 [alerts release]; alerts = nil;
[26]57 [super dealloc];
58}
59
[51]60#pragma mark private
61
[24]62- (void)_setAlarmDate:(NSCalendarDate *)aDate;
63{
64 if (alarmDate != aDate) {
65 [alarmDate release];
66 alarmDate = nil;
67 alarmDate = [aDate retain];
68 }
69}
70
[51]71- (void)_beInvalid:(NSString *)aMessage;
[24]72{
73 alarmType = PSAlarmInvalid;
74 if (aMessage != invalidMessage) {
75 [invalidMessage release];
76 invalidMessage = nil;
77 [self _setAlarmDate: nil];
78 alarmInterval = 0;
79 invalidMessage = [aMessage retain];
80 }
81}
82
[51]83- (void)_beValidWithType:(PSAlarmType)type;
[24]84{
[28]85 if (alarmType == PSAlarmSet) return; // already valid
[24]86 [invalidMessage release];
87 invalidMessage = nil;
88 alarmType = type;
[53]89 if (type != PSAlarmInterval) [self setRepeating: NO];
[24]90}
91
92- (void)_setDateFromInterval;
93{
[53]94 [self _setAlarmDate: [NSCalendarDate dateWithTimeIntervalSinceNow: alarmInterval]];
[51]95 [self _beValidWithType: PSAlarmInterval];
[24]96}
97
[51]98- (void)_setIntervalFromDate;
[24]99{
[53]100 alarmInterval = [alarmDate timeIntervalSinceNow];
[24]101 if (alarmInterval <= 0) {
[51]102 [self _beInvalid: @"Please specify an alarm time in the future."];
103 return;
[24]104 }
[51]105 [self _beValidWithType: PSAlarmDate];
[24]106}
107
[53]108- (PSAlarmType)_alarmTypeForString:(NSString *)string;
109{
110 if ([string isEqualToString: @"PSAlarmDate"]) return PSAlarmDate;
111 if ([string isEqualToString: @"PSAlarmInterval"]) return PSAlarmInterval;
112 if ([string isEqualToString: @"PSAlarmSet"]) return PSAlarmSet;
113 if ([string isEqualToString: @"PSAlarmInvalid"]) return PSAlarmInvalid;
114 if ([string isEqualToString: @"PSAlarmSnooze"]) return PSAlarmSnooze;
115 if ([string isEqualToString: @"PSAlarmExpired"]) return PSAlarmExpired;
116 NSLog(@"unknown alarm type string: %@", string);
117 return nil;
118}
119
[51]120- (NSString *)_alarmTypeString;
[24]121{
[51]122 switch (alarmType) {
123 case PSAlarmDate: return @"PSAlarmDate";
124 case PSAlarmInterval: return @"PSAlarmInterval";
125 case PSAlarmSet: return @"PSAlarmSet";
126 case PSAlarmInvalid: return @"PSAlarmInvalid";
[53]127 case PSAlarmSnooze: return @"PSAlarmSnooze";
128 case PSAlarmExpired: return @"PSAlarmExpired";
[51]129 default: return [NSString stringWithFormat: @"<unknown: %u>", alarmType];
130 }
131}
132
[53]133- (NSString *)_stringForInterval:(unsigned long long)interval;
134{
135 const unsigned long long minute = 60, hour = minute * 60, day = hour * 24, year = day * 365.26;
136 // +[NSString stringWithFormat:] in 10.1 does not support long longs: work around it by converting to unsigned ints or longs for display
137 if (interval == 0) return nil;
138 if (interval < minute) return [NSString stringWithFormat: @"%us", (unsigned)interval];
139 if (interval < day) return [NSString stringWithFormat: @"%uh %um", (unsigned)(interval / hour), (unsigned)((interval % hour) / minute)];
140 if (interval < 2 * day) return @"One day";
141 if (interval < year) return [NSString stringWithFormat: @"%u days", (unsigned)(interval / day)];
142 if (interval < 2 * year) return @"One year";
143 return [NSString stringWithFormat: @"%lu years", (unsigned long)(interval / year)];
144}
145
[61]146- (void)_timerExpired:(PSTimer *)aTimer;
[51]147{
[53]148 NSLog(@"expired: %@; now %@", [[aTimer fireDate] description], [[NSDate date] description]);
149 alarmType = PSAlarmExpired;
[51]150 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmTimerExpiredNotification object: self];
151 [timer release]; timer = nil;
152}
153
154#pragma mark alarm setting
155
156- (void)setInterval:(NSTimeInterval)anInterval;
157{
158 alarmInterval = anInterval;
[24]159 if (alarmInterval <= 0) {
[51]160 [self _beInvalid: @"Please specify an alarm interval."]; return;
[24]161 }
[51]162 [self _setDateFromInterval];
[24]163}
164
[26]165- (void)setForDateAtTime:(NSCalendarDate *)dateTime;
[24]166{
[26]167 [self _setAlarmDate: dateTime];
[24]168 [self _setIntervalFromDate];
169}
170
171- (void)setForDate:(NSDate *)date atTime:(NSDate *)time;
172{
[53]173 NSCalendarDate *dateTime;
[24]174 if (time == nil && date == nil) {
[51]175 [self _beInvalid: @"Please specify an alarm date and time."]; return;
[24]176 }
177 if (time == nil) {
[51]178 [self _beInvalid: @"Please specify an alarm time."]; return;
[24]179 }
180 if (date == nil) {
[51]181 [self _beInvalid: @"Please specify an alarm date."]; return;
[24]182 }
183 // XXX if calTime's date is different from the default date, complain
[53]184 dateTime = [NSCalendarDate dateWithDate: date atTime: time];
185 if (dateTime == nil) {
[105]186 [self _beInvalid: @"Please specify a reasonable date and time."]; return;
[24]187 }
[53]188 [self setForDateAtTime: dateTime];
[24]189}
190
[53]191- (void)setRepeating:(BOOL)isRepeating;
192{
193 repeating = isRepeating;
194}
195
196- (void)setSnoozeInterval:(NSTimeInterval)anInterval;
197{
198 snoozeInterval = anInterval;
[103]199 NSAssert(alarmType == PSAlarmExpired, NSLocalizedString(@"Can't snooze an alarm that hasn't expired", "Assertion for PSAlarm snooze setting"));
[53]200 alarmType = PSAlarmSnooze;
201}
202
[61]203- (void)setWakeUp:(BOOL)doWake;
204{
205 [timer setWakeUp: doWake];
206}
207
[51]208#pragma mark accessing
209
210- (NSString *)message;
[24]211{
[51]212 if (alarmMessage == nil || [alarmMessage isEqualToString: @""])
213 return @"Alarm!";
214 return alarmMessage;
[24]215}
216
217- (void)setMessage:(NSString *)aMessage;
218{
219 if (aMessage != alarmMessage) {
220 [alarmMessage release];
221 alarmMessage = nil;
222 alarmMessage = [aMessage retain];
223 }
224}
225
[51]226- (BOOL)isValid;
[24]227{
[51]228 if (alarmType == PSAlarmDate) [self _setIntervalFromDate];
[53]229 if (alarmType == PSAlarmInvalid ||
230 (alarmType == PSAlarmExpired && ![self isRepeating])) return NO;
231 return YES;
[24]232}
233
234- (NSString *)invalidMessage;
235{
[26]236 if (invalidMessage == nil) return @"";
[24]237 return invalidMessage;
238}
239
[28]240- (NSCalendarDate *)date;
[24]241{
242 if (alarmType == PSAlarmInterval) [self _setDateFromInterval];
243 return alarmDate;
244}
245
[51]246- (NSCalendarDate *)time;
247{
248 if (alarmType == PSAlarmInterval) [self _setDateFromInterval];
249 return [[NSCalendarDate alloc] initWithYear: 0
250 month: 1
251 day: 1
252 hour: [alarmDate hourOfDay]
253 minute: [alarmDate minuteOfHour]
254 second: [alarmDate secondOfMinute]
255 timeZone: nil];
256}
257
258- (NSTimeInterval)interval;
259{
[53]260 if (alarmType == PSAlarmDate) [self _setIntervalFromDate];
[51]261 return alarmInterval;
262}
263
[53]264- (NSTimeInterval)snoozeInterval;
[51]265{
[53]266 return snoozeInterval;
[51]267}
268
[53]269- (NSTimeInterval)timeRemaining;
[51]270{
[103]271 NSAssert1(alarmType == PSAlarmSet, NSLocalizedString(@"Can't get time remaining on alarm with no timer set: %@", "Assertion for PSAlarm time remaining, internal error; %@ replaced by alarm description"), self);
[53]272 return -[[NSDate date] timeIntervalSinceDate: alarmDate];
[51]273}
274
[53]275- (void)setAlerts:(PSAlerts *)theAlerts;
[51]276{
[53]277 [alerts release]; alerts = nil;
278 alerts = [theAlerts retain];
[51]279}
280
[53]281- (PSAlerts *)alerts;
282{
283 if (alerts == nil) alerts = [[PSAlerts alloc] init];
284 return alerts;
285}
286
287- (BOOL)isRepeating;
288{
289 return repeating;
290}
291
[43]292- (NSString *)dateString;
293{
294 return [[self date] descriptionWithCalendarFormat: dateFormat locale: locale];
295}
296
[28]297- (NSString *)shortDateString;
298{
[43]299 return [[self date] descriptionWithCalendarFormat: shortDateFormat locale: locale];
[28]300}
301
302- (NSString *)timeString;
303{
[43]304 return [[self date] descriptionWithCalendarFormat: timeFormat locale: locale];
[28]305}
306
[53]307- (NSString *)dateTimeString;
[28]308{
[53]309 return [NSString stringWithFormat: @"%@ at %@", [self dateString], [self timeString]];
310}
311
312- (NSString *)nextDateTimeString;
313{
314 if (![self isRepeating]) {
315 return nil;
316 } else {
317 NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceNow: [self interval]];
318 NSString *nextDateTimeString = [NSString stringWithFormat: @"%@ at %@",
319 [date descriptionWithCalendarFormat: dateFormat locale: locale],
320 [date descriptionWithCalendarFormat: timeFormat locale: locale]];
321 [date release];
322 return nextDateTimeString;
323 }
324}
325
326- (NSString *)intervalString;
327{
328 const unsigned long long mval = 99, minute = 60, hour = minute * 60;
[28]329 unsigned long long interval = [self interval];
[53]330 if (interval == 0) return nil;
331 if (interval == 1) return @"One second";
332 if (interval == minute) return @"One minute";
333 if (interval % minute == 0) return [NSString stringWithFormat: @"%u minutes", (unsigned)(interval / minute)];
334 if (interval <= mval) return [NSString stringWithFormat: @"%u seconds", (unsigned)interval];
335 if (interval == hour) return @"One hour";
336 if (interval % hour == 0) return [NSString stringWithFormat: @"%u hours", (unsigned)(interval / hour)];
337 if (interval <= mval * minute) return [NSString stringWithFormat: @"%u minutes", (unsigned)(interval / minute)];
338 if (interval <= mval * hour) return [NSString stringWithFormat: @"%u hours", (unsigned)(interval / hour)];
339 return [self _stringForInterval: interval];
[28]340}
341
[53]342- (NSString *)timeRemainingString;
343{
344 NSString *timeRemainingString = [self _stringForInterval: llround([self timeRemaining])];
345
346 if (timeRemainingString == nil) return @"ÇexpiredÈ";
347 return timeRemainingString;
348}
349
350- (NSAttributedString *)prettyDescription;
351{
352 NSMutableAttributedString *string = [[NSMutableAttributedString alloc] init];
353 NSAttributedString *alertList = [alerts prettyList];
354
355 [string appendAttributedString:
[103]356 [[NSString stringWithFormat: NSLocalizedString(@"At alarm time for %@:\n", "Alert list title in pretty description, %@ replaced with message"), [self message]] small]];
[53]357 if (alertList != nil) {
358 [string appendAttributedString: alertList];
359 } else {
360 [string appendAttributedString: [@"Do nothing." small]];
361 }
362 if ([self isRepeating]) {
363 [string appendAttributedString:
364 [[NSString stringWithFormat: @"\nAlarm repeats every %@.", [[self intervalString] lowercaseString]] small]];
365 }
366 return [string autorelease];
367}
368
[51]369#pragma mark actions
[24]370
[26]371- (BOOL)setTimer;
372{
[53]373 if (alarmType == PSAlarmExpired) {
374 if ([self isRepeating]) {
375 [self _setDateFromInterval];
376 } else {
377 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmDiedNotification object: self];
[26]378 return NO;
[53]379 }
380 } else if (alarmType == PSAlarmDate) {
381 if (![self isValid]) return NO;
382 } else if (alarmType == PSAlarmSnooze) {
383 [self _setAlarmDate: [NSCalendarDate dateWithTimeIntervalSinceNow: snoozeInterval]];
384 } else if (alarmType != PSAlarmInterval) {
385 return NO;
[26]386 }
[61]387 timer = [PSTimer scheduledTimerWithTimeInterval: (alarmType == PSAlarmSnooze ? snoozeInterval : alarmInterval) target: self selector: @selector(_timerExpired:) userInfo: nil repeats: NO];
[53]388 if (timer == nil) return NO;
389 [timer retain];
390 alarmType = PSAlarmSet;
[61]391 [alerts prepareForAlarm: self];
392
[53]393 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmTimerSetNotification object: self];
394 // NSLog(@"set: %@; now %@; remaining %@", [[timer fireDate] description], [[NSDate date] description], [self timeRemainingString]);
395 return YES;
[26]396}
397
[51]398- (void)cancelTimer;
[26]399{
[28]400 [timer invalidate]; [timer release]; timer = nil;
[53]401 [self setRepeating: NO];
[26]402}
403
[51]404#pragma mark comparing
[26]405
[51]406- (NSComparisonResult)compareDate:(PSAlarm *)otherAlarm;
[26]407{
408 return [[self date] compare: [otherAlarm date]];
409}
410
[51]411- (NSComparisonResult)compareMessage:(PSAlarm *)otherAlarm;
[34]412{
[51]413 return [[self message] caseInsensitiveCompare: [otherAlarm message]];
[34]414}
415
[51]416#pragma mark printing
[34]417
[26]418- (NSString *)description;
419{
[105]420 return [NSString stringWithFormat: @"%@: type %@ date %@ interval %.1f%@%@",
[26]421 [super description], [self _alarmTypeString], alarmDate, alarmInterval,
[105]422 (repeating ? @" repeating" : @""),
[26]423 (alarmType == PSAlarmInvalid ?
424 [NSString stringWithFormat: @"\ninvalid message: %@", invalidMessage]
425 : (alarmType == PSAlarmSet ?
426 [NSString stringWithFormat: @"\ntimer: %@", timer] : @""))];
427}
428
[53]429#pragma mark property list serialization (Pester 1.1)
[51]430
[53]431- (NSDictionary *)propertyListRepresentation;
432{
433 NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity: 5];
434 if (![self isValid]) return nil;
435 [dict setObject: [self _alarmTypeString] forKey: PLAlarmType];
436 switch (alarmType) {
437 case PSAlarmDate:
438 case PSAlarmSet:
439 [dict setObject: [NSNumber numberWithDouble: [alarmDate timeIntervalSinceReferenceDate]] forKey: PLAlarmDate];
440 case PSAlarmSnooze:
441 case PSAlarmInterval:
442 case PSAlarmExpired:
443 break;
444 default:
[103]445 NSAssert1(NO, NSLocalizedString(@"Can't save alarm type %@", "Assertion for invalid PSAlarm type on string; %@ replaced with alarm type string"), [self _alarmTypeString]);
[53]446 break;
447 }
[105]448 if ((alarmType != PSAlarmSet || repeating) && alarmType != PSAlarmDate) {
449 [dict setObject: [NSNumber numberWithBool: repeating] forKey: PLAlarmRepeating];
450 [dict setObject: [NSNumber numberWithDouble: alarmInterval] forKey: PLAlarmInterval];
451 }
[53]452 if (snoozeInterval != 0)
453 [dict setObject: [NSNumber numberWithDouble: snoozeInterval] forKey: PLAlarmSnoozeInterval];
454 [dict setObject: alarmMessage forKey: PLAlarmMessage];
455 if (alerts != nil) {
456 [dict setObject: [alerts propertyListRepresentation] forKey: PLAlarmAlerts];
457 }
458 return dict;
459}
460
461- (id)initWithPropertyList:(NSDictionary *)dict;
462{
463 if ( (self = [self init]) != nil) {
464 PSAlerts *alarmAlerts;
465 alarmType = [self _alarmTypeForString: [dict objectForRequiredKey: PLAlarmType]];
466 switch (alarmType) {
467 case PSAlarmDate:
468 case PSAlarmSet:
469 { NSCalendarDate *date = [[NSCalendarDate alloc] initWithTimeIntervalSinceReferenceDate: [[dict objectForRequiredKey: PLAlarmDate] doubleValue]];
470 [self _setAlarmDate: date];
471 [date release];
472 }
473 break;
474 case PSAlarmSnooze: // snooze interval set but not confirmed; ignore
475 alarmType = PSAlarmExpired;
476 case PSAlarmInterval:
477 case PSAlarmExpired:
478 break;
479 default:
[103]480 NSAssert1(NO, NSLocalizedString(@"Can't load alarm type %@", "Assertion for invalid PSAlarm type on load; %@ replaced with alarm type string"), [self _alarmTypeString]);
[53]481 break;
482 }
[105]483 repeating = [[dict objectForKey: PLAlarmRepeating] boolValue];
484 if ((alarmType != PSAlarmSet || repeating) && alarmType != PSAlarmDate)
485 alarmInterval = [[dict objectForRequiredKey: PLAlarmInterval] doubleValue];
[53]486 snoozeInterval = [[dict objectForKey: PLAlarmSnoozeInterval] doubleValue];
487 [self setMessage: [dict objectForRequiredKey: PLAlarmMessage]];
488 alarmAlerts = [[PSAlerts alloc] initWithPropertyList: [dict objectForRequiredKey: PLAlarmAlerts]];
489 [self setAlerts: alarmAlerts];
490 [alarmAlerts release];
491 if (alarmType == PSAlarmSet) {
492 alarmType = PSAlarmDate;
[105]493 // don't want to put this logic in setTimer or isValid because it can cause invalid alarms to be set (consider when someone clicks the "repeat" checkbox, then switches to a [nonrepeating, by design] date alarm, and enters a date that has passed: we do -not- want the alarm to magically morph into a repeating interval alarm)
494 if (![self setTimer] && [self isRepeating]) {
495 alarmType = PSAlarmInterval;
496 [self setInterval: [[dict objectForRequiredKey: PLAlarmInterval] doubleValue]];
497 [self setTimer];
498 }
[53]499 }
500 if (alarmType == PSAlarmExpired) {
501 [self setTimer];
502 if (alarmType == PSAlarmExpired) { // failed to restart
503 [self release];
504 self = nil;
505 }
506 }
507 }
508 return self;
509}
510
511#pragma mark archiving (Pester 1.0)
512
[26]513- (void)encodeWithCoder:(NSCoder *)coder;
514{
515 if (![self isValid]) return;
516 [coder encodeValueOfObjCType: @encode(PSAlarmType) at: &alarmType];
517 switch (alarmType) {
518 case PSAlarmDate:
519 case PSAlarmSet:
520 [coder encodeObject: alarmDate];
521 break;
522 case PSAlarmInterval:
523 [coder encodeValueOfObjCType: @encode(NSTimeInterval) at: &alarmInterval];
524 break;
525 default:
526 break;
527 }
528 [coder encodeObject: alarmMessage];
[28]529 // NSLog(@"encoded: %@", self); // XXX happening twice, gdb refuses to show proper backtrace, grr
[26]530 return;
531}
532
533- (id)initWithCoder:(NSCoder *)coder;
534{
[53]535 if ( (self = [self init]) != nil) {
536 PSAlerts *legacyAlerts = [[PSAlerts alloc] initWithPesterVersion1Alerts];
537 [self setAlerts: legacyAlerts];
538 [legacyAlerts release];
[26]539 [coder decodeValueOfObjCType: @encode(PSAlarmType) at: &alarmType];
540 switch (alarmType) {
541 case PSAlarmDate:
542 case PSAlarmSet:
543 [self _setAlarmDate: [coder decodeObject]];
544 break;
545 case PSAlarmInterval:
546 [coder decodeValueOfObjCType: @encode(NSTimeInterval) at: &alarmInterval];
547 break;
548 default:
549 break;
550 }
551 [self setMessage: [coder decodeObject]];
552 if (alarmType == PSAlarmSet) {
553 alarmType = PSAlarmDate;
554 [self setTimer];
555 }
556 }
557 return self;
558}
559
[24]560@end
Note: See TracBrowser for help on using the repository browser.