Adding some consistency and fixing some bugs
[gc-dialer] / dialcentral / alarm_handler.py
1 #!/usr/bin/env python
2
3 from __future__ import with_statement
4
5 import os
6 import time
7 import datetime
8 import ConfigParser
9 import logging
10
11 import dbus
12
13 import dialcentral.util.qt_compat as qt_compat
14 QtCore = qt_compat.QtCore
15 import dialcentral.util.linux as linux_utils
16
17
18 _FREMANTLE_ALARM = "Fremantle"
19 _DIABLO_ALARM = "Diablo"
20 _NO_ALARM = "None"
21
22
23 try:
24         import alarm
25         ALARM_TYPE = _FREMANTLE_ALARM
26 except (ImportError, OSError):
27         try:
28                 import osso.alarmd as alarmd
29                 ALARM_TYPE = _DIABLO_ALARM
30         except (ImportError, OSError):
31                 ALARM_TYPE = _NO_ALARM
32
33
34 _moduleLogger = logging.getLogger(__name__)
35
36
37 def _get_start_time(recurrence):
38         now = datetime.datetime.now()
39         startTimeMinute = now.minute + max(recurrence, 5) # being safe
40         startTimeHour = now.hour + int(startTimeMinute / 60)
41         startTimeMinute = startTimeMinute % 59
42         now.replace(minute=startTimeMinute)
43         timestamp = int(time.mktime(now.timetuple()))
44         return timestamp
45
46
47 def _create_recurrence_mask(recurrence, base):
48         """
49         >>> bin(_create_recurrence_mask(60, 60))
50         '0b1'
51         >>> bin(_create_recurrence_mask(30, 60))
52         '0b1000000000000000000000000000001'
53         >>> bin(_create_recurrence_mask(2, 60))
54         '0b10101010101010101010101010101010101010101010101010101010101'
55         >>> bin(_create_recurrence_mask(1, 60))
56         '0b111111111111111111111111111111111111111111111111111111111111'
57         """
58         mask = 0
59         for i in xrange(base / recurrence):
60                 mask |= 1 << (recurrence * i)
61         return mask
62
63
64 def _unpack_minutes(recurrence):
65         """
66         >>> _unpack_minutes(0)
67         (0, 0, 0)
68         >>> _unpack_minutes(1)
69         (0, 0, 1)
70         >>> _unpack_minutes(59)
71         (0, 0, 59)
72         >>> _unpack_minutes(60)
73         (0, 1, 0)
74         >>> _unpack_minutes(129)
75         (0, 2, 9)
76         >>> _unpack_minutes(5 * 60 * 24 + 3 * 60 + 2)
77         (5, 3, 2)
78         >>> _unpack_minutes(12 * 60 * 24 + 3 * 60 + 2)
79         (5, 3, 2)
80         """
81         minutesInAnHour = 60
82         minutesInDay = 24 * minutesInAnHour
83         minutesInAWeek = minutesInDay * 7
84
85         days = recurrence / minutesInDay
86         daysOfWeek = days % 7
87         recurrence -= days * minutesInDay
88         hours = recurrence / minutesInAnHour
89         recurrence -= hours * minutesInAnHour
90         mins = recurrence % minutesInAnHour
91         recurrence -= mins
92         assert recurrence == 0, "Recurrence %d" % recurrence
93         return daysOfWeek, hours, mins
94
95
96 class _FremantleAlarmHandler(object):
97
98         _INVALID_COOKIE = -1
99         _REPEAT_FOREVER = -1
100         _TITLE = "Dialcentral Notifications"
101         _LAUNCHER = "python %s" % os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
102
103         def __init__(self):
104                 self._recurrence = 5
105
106                 self._alarmCookie = self._INVALID_COOKIE
107                 self._launcher = self._LAUNCHER
108
109         @property
110         def alarmCookie(self):
111                 return self._alarmCookie
112
113         def load_settings(self, config, sectionName):
114                 try:
115                         self._recurrence = config.getint(sectionName, "recurrence")
116                         self._alarmCookie = config.getint(sectionName, "alarmCookie")
117                         launcher = config.get(sectionName, "notifier")
118                         if launcher:
119                                 self._launcher = launcher
120                 except ConfigParser.NoOptionError:
121                         pass
122                 except ConfigParser.NoSectionError:
123                         pass
124
125         def save_settings(self, config, sectionName):
126                 try:
127                         config.set(sectionName, "recurrence", str(self._recurrence))
128                         config.set(sectionName, "alarmCookie", str(self._alarmCookie))
129                         launcher = self._launcher if self._launcher != self._LAUNCHER else ""
130                         config.set(sectionName, "notifier", launcher)
131                 except ConfigParser.NoOptionError:
132                         pass
133                 except ConfigParser.NoSectionError:
134                         pass
135
136         def apply_settings(self, enabled, recurrence):
137                 if recurrence != self._recurrence or enabled != self.isEnabled:
138                         if self.isEnabled:
139                                 self._clear_alarm()
140                         if enabled:
141                                 self._set_alarm(recurrence)
142                 self._recurrence = int(recurrence)
143
144         @property
145         def recurrence(self):
146                 return self._recurrence
147
148         @property
149         def isEnabled(self):
150                 return self._alarmCookie != self._INVALID_COOKIE
151
152         def _set_alarm(self, recurrenceMins):
153                 assert 1 <= recurrenceMins, "Notifications set to occur too frequently: %d" % recurrenceMins
154                 alarmTime = _get_start_time(recurrenceMins)
155
156                 event = alarm.Event()
157                 event.appid = self._TITLE
158                 event.alarm_time = alarmTime
159                 event.recurrences_left = self._REPEAT_FOREVER
160
161                 action = event.add_actions(1)[0]
162                 action.flags |= alarm.ACTION_TYPE_EXEC | alarm.ACTION_WHEN_TRIGGERED
163                 action.command = self._launcher
164
165                 recurrence = event.add_recurrences(1)[0]
166                 recurrence.mask_min |= _create_recurrence_mask(recurrenceMins, 60)
167                 recurrence.mask_hour |= alarm.RECUR_HOUR_DONTCARE
168                 recurrence.mask_mday |= alarm.RECUR_MDAY_DONTCARE
169                 recurrence.mask_wday |= alarm.RECUR_WDAY_DONTCARE
170                 recurrence.mask_mon |= alarm.RECUR_MON_DONTCARE
171                 recurrence.special |= alarm.RECUR_SPECIAL_NONE
172
173                 assert event.is_sane()
174                 self._alarmCookie = alarm.add_event(event)
175
176         def _clear_alarm(self):
177                 if self._alarmCookie == self._INVALID_COOKIE:
178                         return
179                 alarm.delete_event(self._alarmCookie)
180                 self._alarmCookie = self._INVALID_COOKIE
181
182
183 class _DiabloAlarmHandler(object):
184
185         _INVALID_COOKIE = -1
186         _TITLE = "Dialcentral Notifications"
187         _LAUNCHER = "python %s" % os.path.abspath(os.path.join(os.path.dirname(__file__), "alarm_notify.py"))
188         _REPEAT_FOREVER = -1
189
190         def __init__(self):
191                 self._recurrence = 5
192
193                 bus = dbus.SystemBus()
194                 self._alarmdDBus = bus.get_object("com.nokia.alarmd", "/com/nokia/alarmd");
195                 self._alarmCookie = self._INVALID_COOKIE
196                 self._launcher = self._LAUNCHER
197
198         @property
199         def alarmCookie(self):
200                 return self._alarmCookie
201
202         def load_settings(self, config, sectionName):
203                 try:
204                         self._recurrence = config.getint(sectionName, "recurrence")
205                         self._alarmCookie = config.getint(sectionName, "alarmCookie")
206                         launcher = config.get(sectionName, "notifier")
207                         if launcher:
208                                 self._launcher = launcher
209                 except ConfigParser.NoOptionError:
210                         pass
211                 except ConfigParser.NoSectionError:
212                         pass
213
214         def save_settings(self, config, sectionName):
215                 config.set(sectionName, "recurrence", str(self._recurrence))
216                 config.set(sectionName, "alarmCookie", str(self._alarmCookie))
217                 launcher = self._launcher if self._launcher != self._LAUNCHER else ""
218                 config.set(sectionName, "notifier", launcher)
219
220         def apply_settings(self, enabled, recurrence):
221                 if recurrence != self._recurrence or enabled != self.isEnabled:
222                         if self.isEnabled:
223                                 self._clear_alarm()
224                         if enabled:
225                                 self._set_alarm(recurrence)
226                 self._recurrence = int(recurrence)
227
228         @property
229         def recurrence(self):
230                 return self._recurrence
231
232         @property
233         def isEnabled(self):
234                 return self._alarmCookie != self._INVALID_COOKIE
235
236         def _set_alarm(self, recurrence):
237                 assert 1 <= recurrence, "Notifications set to occur too frequently: %d" % recurrence
238                 alarmTime = _get_start_time(recurrence)
239
240                 #Setup the alarm arguments so that they can be passed to the D-Bus add_event method
241                 _DEFAULT_FLAGS = (
242                         alarmd.ALARM_EVENT_NO_DIALOG |
243                         alarmd.ALARM_EVENT_NO_SNOOZE |
244                         alarmd.ALARM_EVENT_CONNECTED
245                 )
246                 action = []
247                 action.extend(['flags', _DEFAULT_FLAGS])
248                 action.extend(['title', self._TITLE])
249                 action.extend(['path', self._launcher])
250                 action.extend([
251                         'arguments',
252                         dbus.Array(
253                                 [alarmTime, int(27)],
254                                 signature=dbus.Signature('v')
255                         )
256                 ])  #int(27) used in place of alarm_index
257
258                 event = []
259                 event.extend([dbus.ObjectPath('/AlarmdEventRecurring'), dbus.UInt32(4)])
260                 event.extend(['action', dbus.ObjectPath('/AlarmdActionExec')])  #use AlarmdActionExec instead of AlarmdActionDbus
261                 event.append(dbus.UInt32(len(action) / 2))
262                 event.extend(action)
263                 event.extend(['time', dbus.Int64(alarmTime)])
264                 event.extend(['recurr_interval', dbus.UInt32(recurrence)])
265                 event.extend(['recurr_count', dbus.Int32(self._REPEAT_FOREVER)])
266
267                 self._alarmCookie = self._alarmdDBus.add_event(*event);
268
269         def _clear_alarm(self):
270                 if self._alarmCookie == self._INVALID_COOKIE:
271                         return
272                 deleteResult = self._alarmdDBus.del_event(dbus.Int32(self._alarmCookie))
273                 self._alarmCookie = self._INVALID_COOKIE
274                 assert deleteResult != -1, "Deleting of alarm event failed"
275
276
277 class _ApplicationAlarmHandler(object):
278
279         _REPEAT_FOREVER = -1
280         _MIN_TO_MS_FACTORY = 1000 * 60
281
282         def __init__(self):
283                 self._timer = QtCore.QTimer()
284                 self._timer.setSingleShot(False)
285                 self._timer.setInterval(5 * self._MIN_TO_MS_FACTORY)
286
287         @property
288         def alarmCookie(self):
289                 return 0
290
291         def load_settings(self, config, sectionName):
292                 try:
293                         self._timer.setInterval(config.getint(sectionName, "recurrence") * self._MIN_TO_MS_FACTORY)
294                 except ConfigParser.NoOptionError:
295                         pass
296                 except ConfigParser.NoSectionError:
297                         pass
298                 self._timer.start()
299
300         def save_settings(self, config, sectionName):
301                 config.set(sectionName, "recurrence", str(self.recurrence))
302
303         def apply_settings(self, enabled, recurrence):
304                 self._timer.setInterval(recurrence * self._MIN_TO_MS_FACTORY)
305                 if enabled:
306                         self._timer.start()
307                 else:
308                         self._timer.stop()
309
310         @property
311         def notifySignal(self):
312                 return self._timer.timeout
313
314         @property
315         def recurrence(self):
316                 return int(self._timer.interval() / self._MIN_TO_MS_FACTORY)
317
318         @property
319         def isEnabled(self):
320                 return self._timer.isActive()
321
322
323 class _NoneAlarmHandler(object):
324
325         def __init__(self):
326                 self._enabled = False
327                 self._recurrence = 5
328
329         @property
330         def alarmCookie(self):
331                 return 0
332
333         def load_settings(self, config, sectionName):
334                 try:
335                         self._recurrence = config.getint(sectionName, "recurrence")
336                         self._enabled = True
337                 except ConfigParser.NoOptionError:
338                         pass
339                 except ConfigParser.NoSectionError:
340                         pass
341
342         def save_settings(self, config, sectionName):
343                 config.set(sectionName, "recurrence", str(self.recurrence))
344
345         def apply_settings(self, enabled, recurrence):
346                 self._enabled = enabled
347
348         @property
349         def recurrence(self):
350                 return self._recurrence
351
352         @property
353         def isEnabled(self):
354                 return self._enabled
355
356
357 _BACKGROUND_ALARM_FACTORY = {
358         _FREMANTLE_ALARM: _FremantleAlarmHandler,
359         _DIABLO_ALARM: _DiabloAlarmHandler,
360         _NO_ALARM: None,
361 }[ALARM_TYPE]
362
363
364 class AlarmHandler(object):
365
366         ALARM_NONE = "No Alert"
367         ALARM_BACKGROUND = "Background Alert"
368         ALARM_APPLICATION = "Application Alert"
369         ALARM_TYPES = [ALARM_NONE, ALARM_BACKGROUND, ALARM_APPLICATION]
370
371         ALARM_FACTORY = {
372                 ALARM_NONE: _NoneAlarmHandler,
373                 ALARM_BACKGROUND: _BACKGROUND_ALARM_FACTORY,
374                 ALARM_APPLICATION: _ApplicationAlarmHandler,
375         }
376
377         def __init__(self):
378                 self._alarms = {self.ALARM_NONE: _NoneAlarmHandler()}
379                 self._currentAlarmType = self.ALARM_NONE
380
381         def load_settings(self, config, sectionName):
382                 try:
383                         self._currentAlarmType = config.get(sectionName, "alarm")
384                 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
385                         _moduleLogger.exception("Falling back to old style")
386                         self._currentAlarmType = self.ALARM_BACKGROUND
387                 if self._currentAlarmType not in self.ALARM_TYPES:
388                         self._currentAlarmType = self.ALARM_NONE
389
390                 self._init_alarm(self._currentAlarmType)
391                 if self._currentAlarmType in self._alarms:
392                         self._alarms[self._currentAlarmType].load_settings(config, sectionName)
393                         if not self._alarms[self._currentAlarmType].isEnabled:
394                                 _moduleLogger.info("Config file lied, not actually enabled")
395                                 self._currentAlarmType = self.ALARM_NONE
396                 else:
397                         _moduleLogger.info("Background alerts not supported")
398                         self._currentAlarmType = self.ALARM_NONE
399
400         def save_settings(self, config, sectionName):
401                 config.set(sectionName, "alarm", self._currentAlarmType)
402                 self._alarms[self._currentAlarmType].save_settings(config, sectionName)
403
404         def apply_settings(self, t, recurrence):
405                 self._init_alarm(t)
406                 newHandler = self._alarms[t]
407                 oldHandler = self._alarms[self._currentAlarmType]
408                 if newHandler != oldHandler:
409                         oldHandler.apply_settings(False, 0)
410                 newHandler.apply_settings(True, recurrence)
411                 self._currentAlarmType = t
412
413         @property
414         def alarmType(self):
415                 return self._currentAlarmType
416
417         @property
418         def backgroundNotificationsSupported(self):
419                 return self.ALARM_FACTORY[self.ALARM_BACKGROUND] is not None
420
421         @property
422         def applicationNotifySignal(self):
423                 self._init_alarm(self.ALARM_APPLICATION)
424                 return self._alarms[self.ALARM_APPLICATION].notifySignal
425
426         @property
427         def recurrence(self):
428                 return self._alarms[self._currentAlarmType].recurrence
429
430         @property
431         def isEnabled(self):
432                 return self._currentAlarmType != self.ALARM_NONE
433
434         @property
435         def alarmCookie(self):
436                 return self._alarms[self._currentAlarmType].alarmCookie
437
438         def _init_alarm(self, t):
439                 if t not in self._alarms and self.ALARM_FACTORY[t] is not None:
440                         self._alarms[t] = self.ALARM_FACTORY[t]()
441
442
443 def run():
444         logFormat = '(%(relativeCreated)5d) %(levelname)-5s %(threadName)s.%(name)s.%(funcName)s: %(message)s'
445         logging.basicConfig(level=logging.DEBUG, format=logFormat)
446         from dialcentral import constants
447         try:
448                 import optparse
449         except ImportError:
450                 return
451
452         parser = optparse.OptionParser()
453         parser.add_option("-x", "--display", action="store_true", dest="display", help="Display data")
454         parser.add_option("-e", "--enable", action="store_true", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
455         parser.add_option("-d", "--disable", action="store_false", dest="enabled", help="Whether the alarm should be enabled or not", default=False)
456         parser.add_option("-r", "--recurrence", action="store", type="int", dest="recurrence", help="How often the alarm occurs", default=5)
457         (commandOptions, commandArgs) = parser.parse_args()
458
459         settingsPath = linux_utils.get_resource_path("config", constants.__app_name__, "settings.ini")
460
461         alarmHandler = AlarmHandler()
462         config = ConfigParser.SafeConfigParser()
463         config.read(settingsPath)
464         alarmHandler.load_settings(config, "alarm")
465
466         if commandOptions.display:
467                 print "Alarm (%s) is %s for every %d minutes" % (
468                         alarmHandler.alarmCookie,
469                         "enabled" if alarmHandler.isEnabled else "disabled",
470                         alarmHandler.recurrence,
471                 )
472         else:
473                 isEnabled = commandOptions.enabled
474                 recurrence = commandOptions.recurrence
475
476                 if alarmHandler.backgroundNotificationsSupported:
477                         enableType = AlarmHandler.ALARM_BACKGROUND
478                 else:
479                         enableType = AlarmHandler.ALARM_APPLICATION
480                 alarmHandler.apply_settings(enableType if isEnabled else AlarmHandler.ALARM_NONE, recurrence)
481
482                 alarmHandler.save_settings(config, "alarm")
483                 with open(settingsPath, "wb") as configFile:
484                         config.write(configFile)
485
486
487 if __name__ == "__main__":
488         run()