Is it possible to use an NSTimer to wake a jailbroken iPhone from deep sleep?
ios, iphone, jailbreak, objective-c
Solution
I've worked out a method that works for me. As per my long exchange with Nate (and I definitely wouldn't have been able to work out what was going on without his help), this seems to happen automatically on some systems, but not on others. The problem on my phone seemed to be that `powerd` was putting the phone into some sort of deep sleep that paused the `NSTimer`s and didn't allow them to fire properly.
Rather than disabling deep sleep (which I suspect has negative power implications) I scheduled a power event:
NSDate *wakeTime = [[NSDate date] dateByAddingTimeInterval:(delayInSeconds - 10)];
int reply = IOPMSchedulePowerEvent((CFDateRef)wakeTime, CFSTR("com.amm.daemontimertest"), CFSTR(kIOPMAutoWake));
This successfully wakes the phone 10 seconds before the alarm is supposed to go off. (The interval isn't precise. I wanted it to be short enough that the phone wouldn't go back to sleep, but long enough that if the phone takes a moment to wake up the timer can still go at the right time. I'll probably shorten it to just 3 or 4 seconds.
The remaining problem is that the `NSTimer` for the alarm itself won't update automatically, and so it'll be late by whatever period the phone was asleep for. To fix this you can cancel and reschedule the NSTimer whenever the phone wakes up. I did this by registering for a notification that the power management system posts whenever the power state changes:
int status, notifyToken;
status = notify_register_dispatch("com.apple.powermanagement.systempowerstate",
¬ifyToken,
dispatch_get_main_queue(), ^(int t) {
// do stuff to cancel currently running timer and schedule a new one here
});
The inefficiency here is that the notification is posted both on sleeps and wakes, but I haven't been able to find an alternative yet.
I hope this is helpful to anyone else who was struggling with this issue.
Problem
Note: further down in the edits there's simple code that generates the problem without the full complexity of my original program. I'm trying to code an alarm-clock app for jailbroken iOS. I have a UI set up as a standalone application for scheduling the alarms, which then saves the alarm information to disk. The save file is read by a launch daemon that's always running, which deals with actually scheduling the alarms. I'm scheduling the alarms as so (EDIT: in the daemon) (`NSDate *fireDate` is calculated earlier): ``` NSTimer *singleTimer = [[NSTimer alloc] initWithFireDate:fireDate interval:0 target:self selector:@selector(soundAlarm:) userInfo:alarm repeats:NO]; [[NSRunLoop currentRunLoop] addTimer:singleTimer forMode:NSRunLoopCommonModes]; [self.timers addObject:singleTimer]; [singleTimer release]; ``` EDIT: the above code runs in a method called `createTimers`, which gets called by `reloadData`. `reloadData` reads information about the timers from the shared save file, and it gets called in `AMMQRDaemonManager`'s init function, as well as whenever the manager gets a notification (with `notify_post`) that the UI app has updated the save file. The `soundAlarm:` method (EDIT: also in the daemon) is: ``` - (void)soundAlarm:(NSTimer *)theTimer { NSLog(@"qralarmdaemon: sounding alarm"); extern CFStringRef kCFUserNotificationAlertTopMostKey; CFMutableDictionaryRef dict = CFDictionaryCreateMutable(NULL, 3, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFDictionaryAddValue(dict, kCFUserNotificationAlertTopMostKey, kCFBooleanTrue); CFDictionaryAddValue(dict, kCFUserNotificationAlertHeaderKey, CFSTR("Title")); CFDictionaryAddValue(dict,kCFUserNotificationDefaultButtonTitleKey, CFSTR("OK")); SInt32 err = 0; CFUserNotificationRef notif = CFUserNotificationCreate(NULL, 0, kCFUserNotificationPlainAlertLevel, &err, dict); CFOptionFlags response; if((err) || (CFUserNotificationReceiveResponse(notif, 0, &response))) { // do stuff } else if((response & 0x3) == kCFUserNotificationDefaultResponse) { // do stuff } CFRelease(dict); CFRelease(notif); // Do some other stuff } ``` This works great, and shows the alert whether the phone is unlocked or locked. But if the phone is locked for a sufficient period of time to enter deep sleep then the timer just fails to fire. I don't need it to necessarily turn the screen on (though that would be nice) since I'll also be playing sound in addition to displaying the alert, but I do need the timer to fire so that I know when to start the sound. Any ideas? EDIT: Here is the `main` function for the daemon. ``` int main(int argc, char **argv, char **envp) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSLog(@"qralarmdaemon: launched"); AMMQRDaemonManager *manager = [[AMMQRDaemonManager alloc] init]; NSTimer *keepRunningTimer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:1000 target:manager selector:@selector(keepRunning:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:keepRunningTimer forMode:NSRunLoopCommonModes]; // Execute run loop NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop run]; [manager release]; NSLog(@"qralarmdaemon: exiting"); [pool release]; return 0; } ``` (Not included is the code that registers for notifications from the main app to know when to read in the save file, etc, but I don't think that's relevant). EDIT (again): I've added a timer to the run loop that fires at `[NSDate distantFuture]`. This seems to preserve the timers longer (a timer scheduled 1 min 45 secs after the phone was locked went off, and woke up the phone) but not indefinitely (a timer scheduled 7 min, 30 seconds after the phone was locked did not go off). EDIT: I've constructed the following toy example that illustrates the problem, without having to worry about interactions with other parts of my code. I compiled this code, SSH'd in, and ran it, then locked my phone. If I change the `dateByAddingTimeInterval:480` to `dateByAddingTimeInterval:30`, I get the following output: ``` 2013-03-31 12:21:25.555 daemontimertest[6160:707] daemon-timer-test: launched 2013-03-31 12:21:56.265 daemontimertest[6160:707] daemon-timer-test: timer fired ``` But when it's set to 480, I wait more than 8 minutes and only see the first line: ``` 2013-03-31 12:08:09.331 daemontimertest[6049:707] daemon-timer-test: launched ``` `main.m`: ``` #import "MyClass.h" int main(int argc, char **argv, char **envp) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSLog(@"daemon-timer-test: launched"); MyClass *obj = [[MyClass alloc] init]; NSTimer *singleTimer = [[NSTimer alloc] initWithFireDate:[[NSDate date] dateByAddingTimeInterval:480] interval:0 target:obj selector:@selector(fireTimer:) userInfo:nil repeats:NO]; [[NSRunLoop currentRunLoop] addTimer:singleTimer forMode:NSRunLoopCommonModes]; // Execute run loop [[NSRunLoop currentRunLoop] run]; [pool release]; return 0; } ``` `MyClass.m`: ``` #import "MyClass.h" @implementation MyClass - (void)fireTimer:(NSTimer *)theTimer { NSLog(@"daemon-timer-test: timer fired"); } @end ``` EDIT (3/31/13 5:50 EDT): I've added the following code the toy app code to incorporate Nate's suggestion of using GCD's `dispatch_after` functionality, but it appears subject to the same time constraints. As an additional note, the main UI app is installed in `/Applications` and the daemon is installed in `/usr/bin`. ``` double delayInSeconds = 10.0; NSLog(@"daemon-timer-test: delay is %f",delayInSeconds); dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ NSLog(@"daemon-timer-test: time has passed."); }); ``` EDIT (3/31 5:54 PM): Another quick note. The following lines show up (not consecutively) in syslog right before it appears to go into deep sleep and there are no more messages before I wake the phone up. I've selected the ones that look like they may be relevant; the last message is the very last one sent to syslog before deep sleep. ``` Mar 31 17:34:23 Andrew-MacKie-Masons-iPhone lockdownd[50]: 002c1000 -[hostWatcher handleSleepNotification:service:messageArgument:]: <hostWatcher: 0x1cd59890> [CC535EDB-0413-4E5E-A844-4DA035E7217C 169.254.2.141:54757] [fd=13]: kIOMessageCanSystemSleep Mar 31 17:34:23 Andrew-MacKie-Masons-iPhone lockdownd[50]: 002c1000 -[hostWatcher handleSleepNotification:service:messageArgument:]: <hostWatcher: 0x1cd59890> [CC535EDB-0413-4E5E-A844-4DA035E7217C 169.254.2.141:54757] [fd=13]: kIOMessageSystemWillSleep ... Mar 31 17:34:29 Andrew-MacKie-Masons-iPhone lockdownd[50]: 00343000 __63-[hostWatcher handleSleepNotification:service:messageArgument:]_block_invoke_0: Allowing Sleep Mar 31 17:34:29 Andrew-MacKie-Masons-iPhone powerd[42]: PM scheduled RTC wake event: WakeImmediate inDelta=645.40 Mar 31 17:34:29 Andrew-MacKie-Masons-iPhone powerd[42]: Idle Sleep Sleep: Using BATT (Charge:76%) ... Mar 31 17:34:29 Andrew-MacKie-Masons-iPhone kernel[0]: en0::stopOutputQueues ... Mar 31 17:34:29 Andrew-MacKie-Masons-iPhone kernel[0]: pmu wake events: menu ```