Results of testing on Maemo 5
[gc-dialer] / src / session.py
1 from __future__ import with_statement
2
3 import os
4 import time
5 import logging
6
7 try:
8         import cPickle
9         pickle = cPickle
10 except ImportError:
11         import pickle
12
13 from PyQt4 import QtCore
14
15 from util import qore_utils
16 from util import concurrent
17 from util import misc as misc_utils
18
19 import constants
20
21
22 _moduleLogger = logging.getLogger(__name__)
23
24
25 class _DraftContact(object):
26
27         def __init__(self, title, description, numbersWithDescriptions):
28                 self.title = title
29                 self.description = description
30                 self.numbers = numbersWithDescriptions
31                 self.selectedNumber = numbersWithDescriptions[0][0]
32
33
34 class Draft(QtCore.QObject):
35
36         sendingMessage = QtCore.pyqtSignal()
37         sentMessage = QtCore.pyqtSignal()
38         calling = QtCore.pyqtSignal()
39         called = QtCore.pyqtSignal()
40         cancelling = QtCore.pyqtSignal()
41         cancelled = QtCore.pyqtSignal()
42         error = QtCore.pyqtSignal(str)
43
44         recipientsChanged = QtCore.pyqtSignal()
45
46         def __init__(self, pool, backend):
47                 QtCore.QObject.__init__(self)
48                 self._contacts = {}
49                 self._pool = pool
50                 self._backend = backend
51
52         def send(self, text):
53                 assert 0 < len(self._contacts)
54                 numbers = [contact.selectedNumber for contact in self._contacts.itervalues()]
55                 le = concurrent.AsyncLinearExecution(self._pool, self._send)
56                 le.start(numbers, text)
57
58         def call(self):
59                 assert len(self._contacts) == 1
60                 (contact, ) = self._contacts.itervalues()
61                 le = concurrent.AsyncLinearExecution(self._pool, self._call)
62                 le.start(contact.selectedNumber)
63
64         def cancel(self):
65                 le = concurrent.AsyncLinearExecution(self._pool, self._cancel)
66                 le.start()
67
68         def add_contact(self, contactId, title, description, numbersWithDescriptions):
69                 assert contactId not in self._contacts
70                 contactDetails = _DraftContact(title, description, numbersWithDescriptions)
71                 self._contacts[contactId] = contactDetails
72                 self.recipientsChanged.emit()
73
74         def remove_contact(self, contactId):
75                 assert contactId in self._contacts
76                 del self._contacts[contactId]
77                 self.recipientsChanged.emit()
78
79         def get_contacts(self):
80                 return self._contacts.iterkeys()
81
82         def get_num_contacts(self):
83                 return len(self._contacts)
84
85         def get_title(self, cid):
86                 return self._contacts[cid].title
87
88         def get_description(self, cid):
89                 return self._contacts[cid].description
90
91         def get_numbers(self, cid):
92                 return self._contacts[cid].numbers
93
94         def get_selected_number(self, cid):
95                 return self._contacts[cid].selectedNumber
96
97         def set_selected_number(self, cid, number):
98                 # @note I'm lazy, this isn't firing any kind of signal since only one
99                 # controller right now and that is the viewer
100                 assert number in (nWD[0] for nWD in self._contacts[cid].numbers)
101                 self._contacts[cid].selectedNumber = number
102
103         def clear(self):
104                 oldContacts = self._contacts
105                 self._contacts = {}
106                 if oldContacts:
107                         self.recipientsChanged.emit()
108
109         def _send(self, numbers, text):
110                 self.sendingMessage.emit()
111                 try:
112                         yield (
113                                 self._backend[0].send_sms,
114                                 (numbers, text),
115                                 {},
116                         )
117                         self.sentMessage.emit()
118                         self.clear()
119                 except Exception, e:
120                         self.error.emit(str(e))
121
122         def _call(self, number):
123                 self.calling.emit()
124                 try:
125                         yield (
126                                 self._backend[0].call,
127                                 (number, ),
128                                 {},
129                         )
130                         self.called.emit()
131                         self.clear()
132                 except Exception, e:
133                         self.error.emit(str(e))
134
135         def _cancel(self):
136                 self.cancelling.emit()
137                 try:
138                         yield (
139                                 self._backend[0].cancel,
140                                 (),
141                                 {},
142                         )
143                         self.cancelled.emit()
144                 except Exception, e:
145                         self.error.emit(str(e))
146
147
148 class Session(QtCore.QObject):
149
150         stateChange = QtCore.pyqtSignal(str)
151         loggedOut = QtCore.pyqtSignal()
152         loggedIn = QtCore.pyqtSignal()
153         callbackNumberChanged = QtCore.pyqtSignal(str)
154
155         contactsUpdated = QtCore.pyqtSignal()
156         messagesUpdated = QtCore.pyqtSignal()
157         historyUpdated = QtCore.pyqtSignal()
158         dndStateChange = QtCore.pyqtSignal(bool)
159
160         error = QtCore.pyqtSignal(str)
161
162         LOGGEDOUT_STATE = "logged out"
163         LOGGINGIN_STATE = "logging in"
164         LOGGEDIN_STATE = "logged in"
165
166         _OLDEST_COMPATIBLE_FORMAT_VERSION = misc_utils.parse_version("1.2.0")
167
168         _LOGGEDOUT_TIME = -1
169         _LOGGINGIN_TIME = 0
170
171         def __init__(self, cachePath = None):
172                 QtCore.QObject.__init__(self)
173                 self._pool = qore_utils.AsyncPool()
174                 self._backend = []
175                 self._loggedInTime = self._LOGGEDOUT_TIME
176                 self._loginOps = []
177                 self._cachePath = cachePath
178                 self._username = None
179                 self._draft = Draft(self._pool, self._backend)
180
181                 self._contacts = {}
182                 self._messages = []
183                 self._history = []
184                 self._dnd = False
185                 self._callback = ""
186
187         @property
188         def state(self):
189                 return {
190                         self._LOGGEDOUT_TIME: self.LOGGEDOUT_STATE,
191                         self._LOGGINGIN_TIME: self.LOGGINGIN_STATE,
192                 }.get(self._loggedInTime, self.LOGGEDIN_STATE)
193
194         @property
195         def draft(self):
196                 return self._draft
197
198         def login(self, username, password):
199                 assert self.state == self.LOGGEDOUT_STATE
200                 assert username != ""
201                 if self._cachePath is not None:
202                         cookiePath = os.path.join(self._cachePath, "%s.cookies" % username)
203                 else:
204                         cookiePath = None
205
206                 if self._username != username or not self._backend:
207                         from backends import gv_backend
208                         del self._backend[:]
209                         self._backend[0:0] = [gv_backend.GVDialer(cookiePath)]
210
211                 self._pool.start()
212                 le = concurrent.AsyncLinearExecution(self._pool, self._login)
213                 le.start(username, password)
214
215         def logout(self):
216                 assert self.state != self.LOGGEDOUT_STATE
217                 self._pool.stop()
218                 self._loggedInTime = self._LOGGEDOUT_TIME
219                 self._backend[0].persist()
220                 self._save_to_cache()
221
222         def clear(self):
223                 assert self.state == self.LOGGEDOUT_STATE
224                 self._backend[0].logout()
225                 del self._backend[0]
226                 self._clear_cache()
227                 self._draft.clear()
228
229         def logout_and_clear(self):
230                 assert self.state != self.LOGGEDOUT_STATE
231                 self._pool.stop()
232                 self._loggedInTime = self._LOGGEDOUT_TIME
233                 self.clear()
234
235         def update_contacts(self, force = True):
236                 if not force and self._contacts:
237                         return
238                 le = concurrent.AsyncLinearExecution(self._pool, self._update_contacts)
239                 self._perform_op_while_loggedin(le)
240
241         def get_contacts(self):
242                 return self._contacts
243
244         def update_messages(self, force = True):
245                 if not force and self._messages:
246                         return
247                 le = concurrent.AsyncLinearExecution(self._pool, self._update_messages)
248                 self._perform_op_while_loggedin(le)
249
250         def get_messages(self):
251                 return self._messages
252
253         def update_history(self, force = True):
254                 if not force and self._history:
255                         return
256                 le = concurrent.AsyncLinearExecution(self._pool, self._update_history)
257                 self._perform_op_while_loggedin(le)
258
259         def get_history(self):
260                 return self._history
261
262         def update_dnd(self):
263                 le = concurrent.AsyncLinearExecution(self._pool, self._update_dnd)
264                 self._perform_op_while_loggedin(le)
265
266         def set_dnd(self, dnd):
267                 # I'm paranoid about our state geting out of sync so we set no matter
268                 # what but act as if we have the cannonical state
269                 assert self.state == self.LOGGEDIN_STATE
270                 oldDnd = self._dnd
271                 try:
272                         yield (
273                                 self._backend[0].set_dnd,
274                                 (dnd),
275                                 {},
276                         )
277                 except Exception, e:
278                         self.error.emit(str(e))
279                         return
280                 self._dnd = dnd
281                 if oldDnd != self._dnd:
282                         self.dndStateChange.emit(self._dnd)
283
284         def get_dnd(self):
285                 return self._dnd
286
287         def get_account_number(self):
288                 return self._backend[0].get_account_number()
289
290         def get_callback_numbers(self):
291                 # @todo Remove evilness
292                 return self._backend[0].get_callback_numbers()
293
294         def get_callback_number(self):
295                 return self._callback
296
297         def set_callback_number(self, callback):
298                 # I'm paranoid about our state geting out of sync so we set no matter
299                 # what but act as if we have the cannonical state
300                 assert self.state == self.LOGGEDIN_STATE
301                 oldCallback = self._callback
302                 try:
303                         yield (
304                                 self._backend[0].set_callback_number,
305                                 (callback),
306                                 {},
307                         )
308                 except Exception, e:
309                         self.error.emit(str(e))
310                         return
311                 self._callback = callback
312                 if oldCallback != self._callback:
313                         self.callbackNumberChanged.emit(self._callback)
314
315         def _login(self, username, password):
316                 self._loggedInTime = self._LOGGINGIN_TIME
317                 self.stateChange.emit(self.LOGGINGIN_STATE)
318                 finalState = self.LOGGEDOUT_STATE
319                 try:
320                         isLoggedIn = False
321
322                         if not isLoggedIn and self._backend[0].is_quick_login_possible():
323                                 isLoggedIn = yield (
324                                         self._backend[0].is_authed,
325                                         (),
326                                         {},
327                                 )
328                                 if isLoggedIn:
329                                         _moduleLogger.info("Logged in through cookies")
330                                 else:
331                                         # Force a clearing of the cookies
332                                         yield (
333                                                 self._backend[0].logout,
334                                                 (),
335                                                 {},
336                                         )
337
338                         if not isLoggedIn:
339                                 isLoggedIn = yield (
340                                         self._backend[0].login,
341                                         (username, password),
342                                         {},
343                                 )
344                                 if isLoggedIn:
345                                         _moduleLogger.info("Logged in through credentials")
346
347                         if isLoggedIn:
348                                 self._loggedInTime = int(time.time())
349                                 oldUsername = self._username
350                                 self._username = username
351                                 finalState = self.LOGGEDIN_STATE
352                                 self.loggedIn.emit()
353                                 if oldUsername != self._username:
354                                         needOps = not self._load()
355                                 else:
356                                         needOps = True
357                                 if needOps:
358                                         loginOps = self._loginOps[:]
359                                 else:
360                                         loginOps = []
361                                 del self._loginOps[:]
362                                 for asyncOp in loginOps:
363                                         asyncOp.start()
364                 except Exception, e:
365                         self.error.emit(str(e))
366                 finally:
367                         self.stateChange.emit(finalState)
368
369         def _load(self):
370                 updateContacts = len(self._contacts) != 0
371                 updateMessages = len(self._messages) != 0
372                 updateHistory = len(self._history) != 0
373                 oldDnd = self._dnd
374                 oldCallback = self._callback
375
376                 self._contacts = {}
377                 self._messages = []
378                 self._history = []
379                 self._dnd = False
380                 self._callback = ""
381
382                 loadedFromCache = self._load_from_cache()
383                 if loadedFromCache:
384                         updateContacts = True
385                         updateMessages = True
386                         updateHistory = True
387
388                 if updateContacts:
389                         self.contactsUpdated.emit()
390                 if updateMessages:
391                         self.messagesUpdated.emit()
392                 if updateHistory:
393                         self.historyUpdated.emit()
394                 if oldDnd != self._dnd:
395                         self.dndStateChange.emit(self._dnd)
396                 if oldCallback != self._callback:
397                         self.callbackNumberChanged.emit(self._callback)
398
399                 return loadedFromCache
400
401         def _load_from_cache(self):
402                 if self._cachePath is None:
403                         return False
404                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
405
406                 try:
407                         with open(cachePath, "rb") as f:
408                                 dumpedData = pickle.load(f)
409                 except (pickle.PickleError, IOError, EOFError, ValueError):
410                         _moduleLogger.exception("Pickle fun loading")
411                         return False
412                 except:
413                         _moduleLogger.exception("Weirdness loading")
414                         return False
415
416                 (
417                         version, build,
418                         contacts, messages, history, dnd, callback
419                 ) = dumpedData
420
421                 if misc_utils.compare_versions(
422                         self._OLDEST_COMPATIBLE_FORMAT_VERSION,
423                         misc_utils.parse_version(version),
424                 ) <= 0:
425                         _moduleLogger.info("Loaded cache")
426                         self._contacts = contacts
427                         self._messages = messages
428                         self._history = history
429                         self._dnd = dnd
430                         self._callback = callback
431                         return True
432                 else:
433                         _moduleLogger.debug(
434                                 "Skipping cache due to version mismatch (%s-%s)" % (
435                                         version, build
436                                 )
437                         )
438                         return False
439
440         def _save_to_cache(self):
441                 _moduleLogger.info("Saving cache")
442                 if self._cachePath is None:
443                         return
444                 cachePath = os.path.join(self._cachePath, "%s.cache" % self._username)
445
446                 try:
447                         dataToDump = (
448                                 constants.__version__, constants.__build__,
449                                 self._contacts, self._messages, self._history, self._dnd, self._callback
450                         )
451                         with open(cachePath, "wb") as f:
452                                 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
453                         _moduleLogger.info("Cache saved")
454                 except (pickle.PickleError, IOError):
455                         _moduleLogger.exception("While saving")
456
457         def _clear_cache(self):
458                 updateContacts = len(self._contacts) != 0
459                 updateMessages = len(self._messages) != 0
460                 updateHistory = len(self._history) != 0
461                 oldDnd = self._dnd
462                 oldCallback = self._callback
463
464                 self._contacts = {}
465                 self._messages = []
466                 self._history = []
467                 self._dnd = False
468                 self._callback = ""
469
470                 if updateContacts:
471                         self.contactsUpdated.emit()
472                 if updateMessages:
473                         self.messagesUpdated.emit()
474                 if updateHistory:
475                         self.historyUpdated.emit()
476                 if oldDnd != self._dnd:
477                         self.dndStateChange.emit(self._dnd)
478                 if oldCallback != self._callback:
479                         self.callbackNumberChanged.emit(self._callback)
480
481                 self._save_to_cache()
482
483         def _update_contacts(self):
484                 try:
485                         self._contacts = yield (
486                                 self._backend[0].get_contacts,
487                                 (),
488                                 {},
489                         )
490                 except Exception, e:
491                         self.error.emit(str(e))
492                         return
493                 self.contactsUpdated.emit()
494
495         def _update_messages(self):
496                 try:
497                         self._messages = yield (
498                                 self._backend[0].get_messages,
499                                 (),
500                                 {},
501                         )
502                 except Exception, e:
503                         self.error.emit(str(e))
504                         return
505                 self.messagesUpdated.emit()
506
507         def _update_history(self):
508                 try:
509                         self._history = yield (
510                                 self._backend[0].get_recent,
511                                 (),
512                                 {},
513                         )
514                 except Exception, e:
515                         self.error.emit(str(e))
516                         return
517                 self.historyUpdated.emit()
518
519         def _update_dnd(self):
520                 oldDnd = self._dnd
521                 try:
522                         self._dnd = yield (
523                                 self._backend[0].is_dnd,
524                                 (),
525                                 {},
526                         )
527                 except Exception, e:
528                         self.error.emit(str(e))
529                         return
530                 if oldDnd != self._dnd:
531                         self.dndStateChange(self._dnd)
532
533         def _perform_op_while_loggedin(self, op):
534                 if self.state == self.LOGGEDIN_STATE:
535                         op.start()
536                 else:
537                         self._push_login_op(op)
538
539         def _push_login_op(self, asyncOp):
540                 assert self.state != self.LOGGEDIN_STATE
541                 if asyncOp in self._loginOps:
542                         _moduleLogger.info("Skipping queueing duplicate op: %r" % asyncOp)
543                         return
544                 self._loginOps.append(asyncOp)