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

Last change on this file since 105 was 105, checked in by Nicholas Riley, 22 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
Line 
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"
10#import "PSAlert.h"
11#import "PSAlerts.h"
12#import "PSTimer.h"
13#import "NJRDateFormatter.h"
14#import "NSCalendarDate-NJRExtensions.h"
15#import "NSDictionary-NJRExtensions.h"
16#import "NSString-NJRExtensions.h"
17
18NSString * const PSAlarmTimerSetNotification = @"PSAlarmTimerSetNotification";
19NSString * const PSAlarmTimerExpiredNotification = @"PSAlarmTimerExpiredNotification";
20NSString * const PSAlarmDiedNotification = @"PSAlarmDiedNotification";
21
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
31static NSString *dateFormat, *shortDateFormat, *timeFormat;
32static NSDictionary *locale;
33
34// XXX need to reset pending alarms after sleep, they "freeze" and never expire.
35
36@implementation PSAlarm
37
38#pragma mark initialize-release
39
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
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;
55 [timer invalidate]; [timer release]; timer = nil;
56 [alerts release]; alerts = nil;
57 [super dealloc];
58}
59
60#pragma mark private
61
62- (void)_setAlarmDate:(NSCalendarDate *)aDate;
63{
64 if (alarmDate != aDate) {
65 [alarmDate release];
66 alarmDate = nil;
67 alarmDate = [aDate retain];
68 }
69}
70
71- (void)_beInvalid:(NSString *)aMessage;
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
83- (void)_beValidWithType:(PSAlarmType)type;
84{
85 if (alarmType == PSAlarmSet) return; // already valid
86 [invalidMessage release];
87 invalidMessage = nil;
88 alarmType = type;
89 if (type != PSAlarmInterval) [self setRepeating: NO];
90}
91
92- (void)_setDateFromInterval;
93{
94 [self _setAlarmDate: [NSCalendarDate dateWithTimeIntervalSinceNow: alarmInterval]];
95 [self _beValidWithType: PSAlarmInterval];
96}
97
98- (void)_setIntervalFromDate;
99{
100 alarmInterval = [alarmDate timeIntervalSinceNow];
101 if (alarmInterval <= 0) {
102 [self _beInvalid: @"Please specify an alarm time in the future."];
103 return;
104 }
105 [self _beValidWithType: PSAlarmDate];
106}
107
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
120- (NSString *)_alarmTypeString;
121{
122 switch (alarmType) {
123 case PSAlarmDate: return @"PSAlarmDate";
124 case PSAlarmInterval: return @"PSAlarmInterval";
125 case PSAlarmSet: return @"PSAlarmSet";
126 case PSAlarmInvalid: return @"PSAlarmInvalid";
127 case PSAlarmSnooze: return @"PSAlarmSnooze";
128 case PSAlarmExpired: return @"PSAlarmExpired";
129 default: return [NSString stringWithFormat: @"<unknown: %u>", alarmType];
130 }
131}
132
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
146- (void)_timerExpired:(PSTimer *)aTimer;
147{
148 NSLog(@"expired: %@; now %@", [[aTimer fireDate] description], [[NSDate date] description]);
149 alarmType = PSAlarmExpired;
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;
159 if (alarmInterval <= 0) {
160 [self _beInvalid: @"Please specify an alarm interval."]; return;
161 }
162 [self _setDateFromInterval];
163}
164
165- (void)setForDateAtTime:(NSCalendarDate *)dateTime;
166{
167 [self _setAlarmDate: dateTime];
168 [self _setIntervalFromDate];
169}
170
171- (void)setForDate:(NSDate *)date atTime:(NSDate *)time;
172{
173 NSCalendarDate *dateTime;
174 if (time == nil && date == nil) {
175 [self _beInvalid: @"Please specify an alarm date and time."]; return;
176 }
177 if (time == nil) {
178 [self _beInvalid: @"Please specify an alarm time."]; return;
179 }
180 if (date == nil) {
181 [self _beInvalid: @"Please specify an alarm date."]; return;
182 }
183 // XXX if calTime's date is different from the default date, complain
184 dateTime = [NSCalendarDate dateWithDate: date atTime: time];
185 if (dateTime == nil) {
186 [self _beInvalid: @"Please specify a reasonable date and time."]; return;
187 }
188 [self setForDateAtTime: dateTime];
189}
190
191- (void)setRepeating:(BOOL)isRepeating;
192{
193 repeating = isRepeating;
194}
195
196- (void)setSnoozeInterval:(NSTimeInterval)anInterval;
197{
198 snoozeInterval = anInterval;
199 NSAssert(alarmType == PSAlarmExpired, NSLocalizedString(@"Can't snooze an alarm that hasn't expired", "Assertion for PSAlarm snooze setting"));
200 alarmType = PSAlarmSnooze;
201}
202
203- (void)setWakeUp:(BOOL)doWake;
204{
205 [timer setWakeUp: doWake];
206}
207
208#pragma mark accessing
209
210- (NSString *)message;
211{
212 if (alarmMessage == nil || [alarmMessage isEqualToString: @""])
213 return @"Alarm!";
214 return alarmMessage;
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
226- (BOOL)isValid;
227{
228 if (alarmType == PSAlarmDate) [self _setIntervalFromDate];
229 if (alarmType == PSAlarmInvalid ||
230 (alarmType == PSAlarmExpired && ![self isRepeating])) return NO;
231 return YES;
232}
233
234- (NSString *)invalidMessage;
235{
236 if (invalidMessage == nil) return @"";
237 return invalidMessage;
238}
239
240- (NSCalendarDate *)date;
241{
242 if (alarmType == PSAlarmInterval) [self _setDateFromInterval];
243 return alarmDate;
244}
245
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{
260 if (alarmType == PSAlarmDate) [self _setIntervalFromDate];
261 return alarmInterval;
262}
263
264- (NSTimeInterval)snoozeInterval;
265{
266 return snoozeInterval;
267}
268
269- (NSTimeInterval)timeRemaining;
270{
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);
272 return -[[NSDate date] timeIntervalSinceDate: alarmDate];
273}
274
275- (void)setAlerts:(PSAlerts *)theAlerts;
276{
277 [alerts release]; alerts = nil;
278 alerts = [theAlerts retain];
279}
280
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
292- (NSString *)dateString;
293{
294 return [[self date] descriptionWithCalendarFormat: dateFormat locale: locale];
295}
296
297- (NSString *)shortDateString;
298{
299 return [[self date] descriptionWithCalendarFormat: shortDateFormat locale: locale];
300}
301
302- (NSString *)timeString;
303{
304 return [[self date] descriptionWithCalendarFormat: timeFormat locale: locale];
305}
306
307- (NSString *)dateTimeString;
308{
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;
329 unsigned long long interval = [self interval];
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];
340}
341
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:
356 [[NSString stringWithFormat: NSLocalizedString(@"At alarm time for %@:\n", "Alert list title in pretty description, %@ replaced with message"), [self message]] small]];
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
369#pragma mark actions
370
371- (BOOL)setTimer;
372{
373 if (alarmType == PSAlarmExpired) {
374 if ([self isRepeating]) {
375 [self _setDateFromInterval];
376 } else {
377 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmDiedNotification object: self];
378 return NO;
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;
386 }
387 timer = [PSTimer scheduledTimerWithTimeInterval: (alarmType == PSAlarmSnooze ? snoozeInterval : alarmInterval) target: self selector: @selector(_timerExpired:) userInfo: nil repeats: NO];
388 if (timer == nil) return NO;
389 [timer retain];
390 alarmType = PSAlarmSet;
391 [alerts prepareForAlarm: self];
392
393 [[NSNotificationCenter defaultCenter] postNotificationName: PSAlarmTimerSetNotification object: self];
394 // NSLog(@"set: %@; now %@; remaining %@", [[timer fireDate] description], [[NSDate date] description], [self timeRemainingString]);
395 return YES;
396}
397
398- (void)cancelTimer;
399{
400 [timer invalidate]; [timer release]; timer = nil;
401 [self setRepeating: NO];
402}
403
404#pragma mark comparing
405
406- (NSComparisonResult)compareDate:(PSAlarm *)otherAlarm;
407{
408 return [[self date] compare: [otherAlarm date]];
409}
410
411- (NSComparisonResult)compareMessage:(PSAlarm *)otherAlarm;
412{
413 return [[self message] caseInsensitiveCompare: [otherAlarm message]];
414}
415
416#pragma mark printing
417
418- (NSString *)description;
419{
420 return [NSString stringWithFormat: @"%@: type %@ date %@ interval %.1f%@%@",
421 [super description], [self _alarmTypeString], alarmDate, alarmInterval,
422 (repeating ? @" repeating" : @""),
423 (alarmType == PSAlarmInvalid ?
424 [NSString stringWithFormat: @"\ninvalid message: %@", invalidMessage]
425 : (alarmType == PSAlarmSet ?
426 [NSString stringWithFormat: @"\ntimer: %@", timer] : @""))];
427}
428
429#pragma mark property list serialization (Pester 1.1)
430
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:
445 NSAssert1(NO, NSLocalizedString(@"Can't save alarm type %@", "Assertion for invalid PSAlarm type on string; %@ replaced with alarm type string"), [self _alarmTypeString]);
446 break;
447 }
448 if ((alarmType != PSAlarmSet || repeating) && alarmType != PSAlarmDate) {
449 [dict setObject: [NSNumber numberWithBool: repeating] forKey: PLAlarmRepeating];
450 [dict setObject: [NSNumber numberWithDouble: alarmInterval] forKey: PLAlarmInterval];
451 }
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:
480 NSAssert1(NO, NSLocalizedString(@"Can't load alarm type %@", "Assertion for invalid PSAlarm type on load; %@ replaced with alarm type string"), [self _alarmTypeString]);
481 break;
482 }
483 repeating = [[dict objectForKey: PLAlarmRepeating] boolValue];
484 if ((alarmType != PSAlarmSet || repeating) && alarmType != PSAlarmDate)
485 alarmInterval = [[dict objectForRequiredKey: PLAlarmInterval] doubleValue];
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;
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 }
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
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];
529 // NSLog(@"encoded: %@", self); // XXX happening twice, gdb refuses to show proper backtrace, grr
530 return;
531}
532
533- (id)initWithCoder:(NSCoder *)coder;
534{
535 if ( (self = [self init]) != nil) {
536 PSAlerts *legacyAlerts = [[PSAlerts alloc] initWithPesterVersion1Alerts];
537 [self setAlerts: legacyAlerts];
538 [legacyAlerts release];
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
560@end
Note: See TracBrowser for help on using the repository browser.