Handle for missed calls on startup
[callnotify] / src / usr / lib / hildon-desktop / CallNotify.py
1 import gtk, gst
2 import gobject
3 import hildon, hildondesktop
4 import sqlite3
5 import time
6 import dbus
7 import osso
8 import atexit, os, datetime
9 from dbus.mainloop.glib import DBusGMainLoop
10
11 WriteLog = False
12
13 class Playbin2:
14         def __init__(self):
15                 self.idle = True # not playing at the moment
16                 self.configDir = "/home/user/.config/CallNotify/"
17                 self.Debug = WriteLog
18                 # create a playbin2 pipe
19                 self.player = gst.element_factory_make("playbin2", "player")
20                 # connect a signal handler to it's bus
21                 bus = self.player.get_bus()
22                 bus.add_signal_watch()
23                 bus.connect("message", self.on_message)
24
25         def on_message(self, bus, message):
26                 t = message.type
27                 if t == gst.MESSAGE_EOS:
28                         self.player.set_state(gst.STATE_NULL)
29                         self.idle = True
30                         self.dbg('Playbin2: EOS: STATE_NULL')
31                 elif t == gst.MESSAGE_ERROR:
32                         #err, debug = message.parse_error()
33                         #print >> sys.stderr, "Error: {0} {1}".format(err, debug)
34                         self.player.set_state(gst.STATE_NULL)
35                         self.idle = True
36                         self.dbg('Playbin2: ERROR: STATE_NULL')
37                 return self.idle
38
39         def play(self, file, volume):
40                 # abort previous play if still busy
41                 if not self.idle:
42                         #print >> sys.stderr, 'audio truncated'
43                         self.player.set_state(gst.STATE_NULL)
44                 self.player.set_property("uri", "file://" + file)
45                 if volume > 0.0:
46                         self.player.set_property("volume", min(volume, 1.0))
47                 self.dbg('Volume:' + str(self.player.get_property("volume")))
48                 self.player.set_state(gst.STATE_PLAYING)
49                 self.idle = False # now playing
50
51         def dbg(self, txt):
52                         if self.Debug:
53                                 f = open(self.configDir+'log.txt', 'a')
54                                 f.write(str(datetime.datetime.now()) + ': '+ txt)
55                                 f.write('\n')
56
57                                 f.close()
58
59 class CallNotify(hildondesktop.StatusMenuItem):
60         def __init__(self):
61                 hildondesktop.StatusMenuItem.__init__(self)
62                 # Set members
63                 self.configDir = "/home/user/.config/CallNotify/"
64                 self.configFile = "conf.txt"
65                 self.configSoundFile = "sound.txt"
66                 self.Debug = WriteLog
67                 self.dbg("debugging started")
68                 self.msgType = ""
69                 self.toShow = True
70                 self.stop = False
71                 self.path = "/home/user/.config/hildon-desktop/notifications.db"
72                 self.mainLoop = None
73                 self.soundFile = "/usr/share/CallNotify/missed.wav"
74                 self.soundCall = self.soundFile
75                 self.soundSMS = self.soundFile
76                 self.soundBoth = self.soundFile
77                 self.volume = 0.5
78                 self.visual = True
79                 self.sound = True
80                 self.vibration = True
81                 self.interval = float(5.0)
82                 self.readConfigurationFile()
83
84                 self.dbg('constructor')
85
86                 # Load images
87                 self.loadImages()
88
89                 # Register to handle screen off/on events
90                 osso_c = osso.Context("osso_test_device_on", "0.0.1", False)
91                 device = osso.DeviceState(osso_c)
92
93                 # add d-bus listener for removing notification after viewing missed call
94                 # Doing timeout_add with return False instead of explicitly raising a thread
95                 gobject.timeout_add(500, self.startDbusListeners)
96                 gobject.timeout_add(100, self.handleMissed)
97
98                 self.dbg('constructor end')
99
100         def checkForConfigFile(self):
101                 self.dbg('checkForConfigFile started')
102                 os.umask(0)
103                 if not os.path.exists(self.configDir):
104                                 os.mkdir(self.configDir)
105                 if not os.path.exists(self.configDir+self.configFile):
106                                 a = open(self.configDir+self.configFile,'w+')
107                                 a.write('y;y;y;5.0\n')
108                                 a.close()
109                 if not os.path.exists(self.configDir+self.configSoundFile):
110                                 a = open(self.configDir+self.configSoundFile,'w+')
111                                 a.write('\n')
112                                 a.close()
113                 # set proper permissions
114                 os.system("chmod 755 " + self.configDir)
115                 os.system("chmod 644 " + self.configDir+self.configFile)
116                 os.system("chmod 644 " + self.configDir+self.configSoundFile)
117
118
119         def readConfigurationFile(self):
120                 try:
121                         self.dbg('readConfigurationFile started')
122                         self.checkForConfigFile()
123                         f = open(self.configDir+self.configFile, 'r')
124                         raw_set = f.readline().rsplit(';')
125                         self.visual = raw_set[0] in ('y')
126                         self.sound = raw_set[1] in ('y')
127                         self.vibration = raw_set[2] in ('y')
128                         self.interval = float(raw_set[3].replace(',','.'))
129                         self.dbg('visual='+str(self.visual)+' sound='+str(self.sound)+' vibration='+str(self.vibration)+' interval='+str(self.interval))
130                         f.close()
131
132                         # read sound config file
133                         f = open(self.configDir+self.configSoundFile, 'r')
134                         line = f.readline()
135                         # check if specific missed call, SMS or common sound was defined in config file
136                         if line.strip():
137                                 self.soundCall = line.strip()
138                                 self.dbg('soundCall changed to: '+self.soundCall)
139                         line = f.readline()
140                         if line.strip():
141                                 self.soundSMS = line.rstrip()
142                                 self.dbg('soundSMS changed to: '+self.soundSMS)
143                         line = f.readline()
144                         if line.strip():
145                                 self.soundBoth = line.strip()
146                                 self.dbg('soundBoth changed to: '+self.soundBoth)
147                         line = f.readline()
148                         if line.strip():
149                                 self.volume = float(line.strip())
150                                 self.dbg('volume changed to: '+self.volume)
151                         f.close()
152                 except:
153                         os.remove(self.configDir+self.configFile)
154                         os.remove(self.configDir+self.configSoundFile)
155                         self.checkForConfigFile()
156
157         def playSound(self):
158                 self.dbg('playSound started')
159                 profiled = dbus.Interface(dbus.SessionBus().get_object("com.nokia.profiled", "/com/nokia/profiled"), "com.nokia.profiled")
160                 mce = dbus.Interface(dbus.SystemBus().get_object("com.nokia.mce", "/com/nokia/mce/request"), "com.nokia.mce.request")
161                 if self.sound and profiled.get_value("", "ringing.alert.type") != "silent":
162                         # Create the player_name sink
163                         if self.msgType == "Call" and profiled.get_value("", "ringing.alert.volume") != "0":
164                                 self.dbg('play soundCall:' + self.soundCall)
165                                 Playbin2().play(self.soundCall, self.volume)
166                         elif self.msgType == "SMS" and profiled.get_value("", "sms.alert.volume") != "0":
167                                 self.dbg('play soundSMS:' + self.soundSMS)
168                                 Playbin2().play(self.soundSMS, self.volume)
169                         elif self.msgType == "Both":
170                                 self.dbg('play soundBoth:' + self.soundBoth)
171                                 Playbin2().play(self.soundBoth, self.volume)
172                         else:
173                                 Playbin2().play(self.soundFile, self.volume)
174
175                 if self.vibration and profiled.get_value("", "vibrating.alert.enabled") == "On":
176                         self.dbg('vibrate:')
177                         mce.req_vibrator_pattern_activate("PatternIncomingCall")
178                         time.sleep(0.5);
179                         mce.req_vibrator_pattern_deactivate("PatternIncomingCall")
180                 return True
181
182         def loadImages(self):
183                 self.dbg('loadImages started')
184                 icon_theme = gtk.icon_theme_get_default()
185                 self.callPicture = gtk.gdk.pixbuf_new_from_file_at_size("/usr/share/CallNotify/call.png",18,18)
186                 self.smsPicture = gtk.gdk.pixbuf_new_from_file_at_size("/usr/share/CallNotify/sms.png",18,18)
187
188                 # Load 5 numbers and the "+5"
189                 self.imgList = []
190                 self.imgList.append(gtk.gdk.pixbuf_new_from_file_at_size("/usr/share/CallNotify/1.png",18,18))
191                 self.imgList.append(gtk.gdk.pixbuf_new_from_file_at_size("/usr/share/CallNotify/2.png",18,18))
192                 self.imgList.append(gtk.gdk.pixbuf_new_from_file_at_size("/usr/share/CallNotify/3.png",18,18))
193                 self.imgList.append(gtk.gdk.pixbuf_new_from_file_at_size("/usr/share/CallNotify/4.png",18,18))
194                 self.imgList.append(gtk.gdk.pixbuf_new_from_file_at_size("/usr/share/CallNotify/5.png",18,18))
195                 self.imgList.append(gtk.gdk.pixbuf_new_from_file_at_size("/usr/share/CallNotify/more.png",18,18))
196
197         def startDbusListeners(self):
198                 self.dbg('startDbusListeners started')
199                 DBusGMainLoop(set_as_default=True)
200                 bus = dbus.SessionBus()
201
202                 bus.add_signal_receiver(self.notificationClosed, "NotificationClosed", "org.freedesktop.Notifications", None, "/org/freedesktop/Notifications")
203                 bus.add_signal_receiver(self.pendingMessagesRemoved, "PendingMessagesRemoved", None, None, None)
204                 bus.add_signal_receiver(self.newEvent, "NewEvent", None, None, None)
205
206                 self.mainLoop = gobject.MainLoop()
207                 self.mainLoop.run()
208                 return False
209
210         def newEvent(self, a, b, c, d, e, f):
211                 self.dbg('newEvent started')
212                 # On NewEvent the notifications.db is not immediately filled, thus check the event after one minute waiting
213                 self.tmr_main = gobject.timeout_add(20000, self.handleMissed)
214                 return True
215
216         def notificationClosed(self, a):
217                 self.dbg('notificationClosed started')
218                 self.stop_notification(a)
219
220         def pendingMessagesRemoved(self, a):
221                 self.dbg('pendingMessagesRemoved started')
222                 self.stop_notification(self)
223
224         def handleMissed(self):
225                 missedCall = self.getMissedCallsCount(False)
226                 missedSMS = self.getMissedCallsCount(True)
227                 self.dbg('Missed CALL: ' + str(missedCall))
228                 self.dbg('Missed SMS: ' + str(missedSMS))
229
230                 if missedCall and missedSMS:
231                         self.msgType = "Both"
232                         self.dbg('***********handleMissed BOTH started***********: ' + str(missedCall) + str(missedSMS))
233                 elif missedCall and not missedSMS:
234                         self.msgType = "Call"
235                         self.dbg('***********handleMissed CALL started***********: ' + str(missedCall))
236                 elif missedSMS and not missedCall:
237                         self.msgType = "SMS"
238                         self.dbg('***********handleMissed SMS started***********: ' + str(missedSMS))
239
240                 if missedCall or missedSMS:
241                         self.show()
242
243                 # Execute the function only once on NewEvent
244                 return False
245
246         def stop_notification(self, a):
247                 self.dbg('stop_notification started')
248                 try:
249                         self.set_status_area_icon(None)
250                         # Reset the notification (get recent missed call count)
251                         self.stop = False
252                         self.msgType = ""
253                         gobject.source_remove(self.tmr_ptr)
254                         gobject.source_remove(self.tmr_ptr2)
255                         gobject.source_remove(self.tmr_main)
256                 except:
257                         pass
258
259         def getMissedCallsCount(self, isSms):
260                 conn = sqlite3.connect(self.path)
261                 cur = conn.cursor()
262                 if isSms:
263                         cur.execute("select count(id) from notifications where icon_name='general_sms'")
264                 else:
265                         cur.execute("select count(id) from notifications where icon_name='general_missed'")
266                 missed = cur.fetchone()[0]
267
268                 #if isSms:
269                         #self.dbg('get missed SMSs: ' + str(missed))
270                 #else:
271                         #self.dbg('get missed Calls: ' + str(missed))
272
273                 return missed
274
275         def show(self):
276                 self.dbg('show started')
277                 # blink the icon every 1 second
278                 if not(self.stop):
279                         self.readConfigurationFile()
280                         self.stop = True
281                         if self.visual:
282                                 self.tmr_ptr = gobject.timeout_add(1000, self.blinkIcon)
283                         self.tmr_ptr2 = gobject.timeout_add(int(self.interval*1000*60), self.playSound)
284
285         def blinkIcon(self):
286                 # self.dbg('blinkIcon started')
287                 if self.toShow:
288                         self.toShow = False
289                         img = self.callPicture
290                         if self.msgType == "SMS":
291                                 img = self.smsPicture
292                         self.set_status_area_icon(img)
293                         return True
294                 else:
295                         img = self.smsPicture
296                         isSMS = False
297                         if self.msgType == "SMS":
298                                 isSMS = True
299                         index = self.getMissedCallsCount(isSMS) - 1
300                         if index >= 5:
301                                 index = 5
302                         if index < 0:
303                                 index = 0
304                         if self.msgType != "Both":
305                                 img = self.imgList[index]
306                         self.toShow = True
307                         self.set_status_area_icon(img)
308                         return True
309
310         def dbg(self, txt):
311                         if self.Debug:
312                                 f = open(self.configDir+'log.txt', 'a')
313                                 f.write(str(datetime.datetime.now()) + ': '+ txt)
314                                 f.write('\n')
315
316                                 f.close()
317
318 hd_plugin_type = CallNotify
319
320
321 # Uncomment from "if __name__..." to "gtk.main()" if running from CLI as:
322 # "run-standalone.sh python CallNotify.py"
323
324 #if __name__=="__main__":
325 #               gobject.type_register(hd_plugin_type)
326 #               obj = gobject.new(hd_plugin_type, plugin_id="plugid_id")
327 #               obj.show_all()
328 #               gtk.main()
329