Fixing issues with error reporting
[gc-dialer] / src / gv_views.py
1 #!/usr/bin/env python
2
3 """
4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 @todo Collapse voicemails
22 """
23
24 from __future__ import with_statement
25
26 import re
27 import ConfigParser
28 import itertools
29 import logging
30
31 import gobject
32 import pango
33 import gtk
34
35 import gtk_toolbox
36 import hildonize
37 from backends import gv_backend
38 from backends import null_backend
39
40
41 _moduleLogger = logging.getLogger("gv_views")
42
43
44 def make_ugly(prettynumber):
45         """
46         function to take a phone number and strip out all non-numeric
47         characters
48
49         >>> make_ugly("+012-(345)-678-90")
50         '+01234567890'
51         """
52         return normalize_number(prettynumber)
53
54
55 def normalize_number(prettynumber):
56         """
57         function to take a phone number and strip out all non-numeric
58         characters
59
60         >>> normalize_number("+012-(345)-678-90")
61         '+01234567890'
62         >>> normalize_number("1-(345)-678-9000")
63         '13456789000'
64         >>> normalize_number("+1-(345)-678-9000")
65         '+13456789000'
66         """
67         uglynumber = re.sub('[^0-9+]', '', prettynumber)
68
69         return uglynumber
70
71
72 def _make_pretty_with_areacodde(phonenumber):
73         prettynumber = "(%s)" % (phonenumber[0:3], )
74         if 3 < len(phonenumber):
75                 prettynumber += " %s" % (phonenumber[3:6], )
76                 if 6 < len(phonenumber):
77                         prettynumber += "-%s" % (phonenumber[6:], )
78         return prettynumber
79
80
81 def _make_pretty_local(phonenumber):
82         prettynumber = "%s" % (phonenumber[0:3], )
83         if 3 < len(phonenumber):
84                 prettynumber += "-%s" % (phonenumber[3:], )
85         return prettynumber
86
87
88 def _make_pretty_international(phonenumber):
89         prettynumber = phonenumber
90         if phonenumber.startswith("0"):
91                 prettynumber = "+%s " % (phonenumber[0:3], )
92                 if 3 < len(phonenumber):
93                         prettynumber += _make_pretty_with_areacodde(phonenumber[3:])
94         if phonenumber.startswith("1"):
95                 prettynumber = "1 "
96                 prettynumber += _make_pretty_with_areacodde(phonenumber[1:])
97         return prettynumber
98
99
100 def make_pretty(phonenumber):
101         """
102         Function to take a phone number and return the pretty version
103         pretty numbers:
104                 if phonenumber begins with 0:
105                         ...-(...)-...-....
106                 if phonenumber begins with 1: ( for gizmo callback numbers )
107                         1 (...)-...-....
108                 if phonenumber is 13 digits:
109                         (...)-...-....
110                 if phonenumber is 10 digits:
111                         ...-....
112         >>> make_pretty("12")
113         '12'
114         >>> make_pretty("1234567")
115         '123-4567'
116         >>> make_pretty("2345678901")
117         '+1 (234) 567-8901'
118         >>> make_pretty("12345678901")
119         '+1 (234) 567-8901'
120         >>> make_pretty("01234567890")
121         '+012 (345) 678-90'
122         >>> make_pretty("+01234567890")
123         '+012 (345) 678-90'
124         >>> make_pretty("+12")
125         '+1 (2)'
126         >>> make_pretty("+123")
127         '+1 (23)'
128         >>> make_pretty("+1234")
129         '+1 (234)'
130         """
131         if phonenumber is None or phonenumber is "":
132                 return ""
133
134         phonenumber = normalize_number(phonenumber)
135
136         if phonenumber[0] == "+":
137                 prettynumber = _make_pretty_international(phonenumber[1:])
138                 if not prettynumber.startswith("+"):
139                         prettynumber = "+"+prettynumber
140         elif 8 < len(phonenumber) and phonenumber[0] in ("0", "1"):
141                 prettynumber = _make_pretty_international(phonenumber)
142         elif 7 < len(phonenumber):
143                 prettynumber = _make_pretty_with_areacodde(phonenumber)
144         elif 3 < len(phonenumber):
145                 prettynumber = _make_pretty_local(phonenumber)
146         else:
147                 prettynumber = phonenumber
148         return prettynumber.strip()
149
150
151 def abbrev_relative_date(date):
152         """
153         >>> abbrev_relative_date("42 hours ago")
154         '42 h'
155         >>> abbrev_relative_date("2 days ago")
156         '2 d'
157         >>> abbrev_relative_date("4 weeks ago")
158         '4 w'
159         """
160         parts = date.split(" ")
161         return "%s %s" % (parts[0], parts[1][0])
162
163
164 def _collapse_message(messageLines, maxCharsPerLine, maxLines):
165         lines = 0
166
167         numLines = len(messageLines)
168         for line in messageLines[0:min(maxLines, numLines)]:
169                 linesPerLine = max(1, int(len(line) / maxCharsPerLine))
170                 allowedLines = maxLines - lines
171                 acceptedLines = min(allowedLines, linesPerLine)
172                 acceptedChars = acceptedLines * maxCharsPerLine
173
174                 if acceptedChars < (len(line) + 3):
175                         suffix = "..."
176                 else:
177                         acceptedChars = len(line) # eh, might as well complete the line
178                         suffix = ""
179                 abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
180                 yield abbrevMessage
181
182                 lines += acceptedLines
183                 if maxLines <= lines:
184                         break
185
186
187 def collapse_message(message, maxCharsPerLine, maxLines):
188         r"""
189         >>> collapse_message("Hello", 60, 2)
190         'Hello'
191         >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
192         'Hello world how are you doing today? 01234567890123456789012...'
193         >>> collapse_message('''Hello world how are you doing today?
194         ... 01234567890123456789
195         ... 01234567890123456789
196         ... 01234567890123456789
197         ... 01234567890123456789''', 60, 2)
198         'Hello world how are you doing today?\n01234567890123456789'
199         >>> collapse_message('''
200         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
201         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
202         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
203         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
204         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
205         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2)
206         '\nHello world how are you doing today? 01234567890123456789012...'
207         """
208         messageLines = message.split("\n")
209         return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
210
211
212 def _get_contact_numbers(backend, contactId, number):
213         if contactId:
214                 contactPhoneNumbers = list(backend.get_contact_details(contactId))
215                 uglyContactNumbers = (
216                         make_ugly(contactNumber)
217                         for (numberDescription, contactNumber) in contactPhoneNumbers
218                 )
219                 defaultMatches = [
220                         (
221                                 number == contactNumber or
222                                 number[1:] == contactNumber and number.startswith("1") or
223                                 number[2:] == contactNumber and number.startswith("+1") or
224                                 number == contactNumber[1:] and contactNumber.startswith("1") or
225                                 number == contactNumber[2:] and contactNumber.startswith("+1")
226                         )
227                         for contactNumber in uglyContactNumbers
228                 ]
229                 try:
230                         defaultIndex = defaultMatches.index(True)
231                 except ValueError:
232                         contactPhoneNumbers.append(("Other", number))
233                         defaultIndex = len(contactPhoneNumbers)-1
234                         _moduleLogger.warn(
235                                 "Could not find contact %r's number %s among %r" % (
236                                         contactId, number, contactPhoneNumbers
237                                 )
238                         )
239         else:
240                 contactPhoneNumbers = [("Phone", number)]
241                 defaultIndex = -1
242
243         return contactPhoneNumbers, defaultIndex
244
245
246 class SmsEntryWindow(object):
247
248         MAX_CHAR = 160
249
250         def __init__(self, widgetTree, parent, app):
251                 self._clipboard = gtk.clipboard_get()
252                 self._widgetTree = widgetTree
253                 self._parent = parent
254                 self._app = app
255                 self._isFullScreen = False
256
257                 self._window = self._widgetTree.get_widget("smsWindow")
258                 self._window = hildonize.hildonize_window(self._app, self._window)
259                 self._window.connect("delete-event", self._on_delete)
260                 self._window.connect("key-press-event", self._on_key_press)
261                 self._window.connect("window-state-event", self._on_window_state_change)
262                 self._widgetTree.get_widget("smsMessagesViewPort").get_parent().show()
263
264                 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
265                 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
266                 errorClose = self._widgetTree.get_widget("smsErrorClose")
267                 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
268
269                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
270                 self._smsButton.connect("clicked", self._on_send)
271                 self._dialButton = self._widgetTree.get_widget("dialButton")
272                 self._dialButton.connect("clicked", self._on_dial)
273
274                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
275
276                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
277                 self._messagesView = self._widgetTree.get_widget("smsMessages")
278
279                 textrenderer = gtk.CellRendererText()
280                 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
281                 textrenderer.set_property("wrap-width", 450)
282                 messageColumn = gtk.TreeViewColumn("")
283                 messageColumn.pack_start(textrenderer, expand=True)
284                 messageColumn.add_attribute(textrenderer, "markup", 0)
285                 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
286                 self._messagesView.append_column(messageColumn)
287                 self._messagesView.set_headers_visible(False)
288                 self._messagesView.set_model(self._messagemodel)
289                 self._messagesView.set_fixed_height_mode(False)
290
291                 self._conversationView = self._messagesView.get_parent()
292                 self._conversationViewPort = self._conversationView.get_parent()
293                 self._scrollWindow = self._conversationViewPort.get_parent()
294
295                 self._targetList = self._widgetTree.get_widget("smsTargetList")
296                 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
297                 self._phoneButton.connect("clicked", self._on_phone)
298                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
299                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
300                 self._smsEntrySize = None
301
302                 self._contacts = []
303
304         def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
305                 contactNumbers = list(self._to_contact_numbers(contactDetails))
306                 assert contactNumbers, "Contact must have at least one number"
307                 contactIndex = defaultIndex if defaultIndex != -1 else 0
308                 contact = contactNumbers, contactIndex, messages
309                 self._contacts.append(contact)
310
311                 nameLabel = gtk.Label(name)
312                 selector = gtk.Button(contactNumbers[0][1])
313                 if len(contactNumbers) == 1:
314                         selector.set_sensitive(False)
315                 removeContact = gtk.Button(stock="gtk-delete")
316                 row = gtk.HBox()
317                 row.pack_start(nameLabel, True, True)
318                 row.pack_start(selector, True, True)
319                 row.pack_start(removeContact, False, False)
320                 row.show_all()
321                 self._targetList.pack_start(row)
322                 selector.connect("clicked", self._on_choose_phone_n, row)
323                 removeContact.connect("clicked", self._on_remove_phone_n, row)
324                 self._update_button_state()
325                 self._update_context()
326
327                 parentSize = self._parent.get_size()
328                 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
329                 self._window.show()
330                 self._window.present()
331
332                 self._smsEntry.grab_focus()
333                 self._scroll_to_bottom()
334
335         def clear(self):
336                 del self._contacts[:]
337
338                 for row in list(self._targetList.get_children()):
339                         self._targetList.remove(row)
340                 self._smsEntry.get_buffer().set_text("")
341                 self._update_letter_count()
342                 self._update_context()
343
344         def fullscreen(self):
345                 self._window.fullscreen()
346
347         def unfullscreen(self):
348                 self._window.unfullscreen()
349
350         def _remove_contact(self, contactIndex):
351                 del self._contacts[contactIndex]
352
353                 row = list(self._targetList.get_children())[contactIndex]
354                 self._targetList.remove(row)
355                 self._update_button_state()
356                 self._update_context()
357                 self._scroll_to_bottom()
358
359         def _scroll_to_bottom(self):
360                 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
361                 dx = max(dx, 0)
362                 adjustment = self._scrollWindow.get_vadjustment()
363                 adjustment.value = dx
364
365         def _update_letter_count(self):
366                 if self._smsEntrySize is None:
367                         self._smsEntrySize = self._smsEntry.size_request()
368                 else:
369                         self._smsEntry.set_size_request(*self._smsEntrySize)
370                 entryLength = self._smsEntry.get_buffer().get_char_count()
371
372                 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
373                 if numTexts:
374                         self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
375                 else:
376                         self._letterCountLabel.set_text("%s" % (numCharInText, ))
377
378                 self._update_button_state()
379
380         def _update_context(self):
381                 self._messagemodel.clear()
382                 if len(self._contacts) == 0:
383                         self._messagesView.hide()
384                         self._targetList.hide()
385                         self._phoneButton.hide()
386                         self._phoneButton.set_label("Error: You shouldn't see this")
387                 elif len(self._contacts) == 1:
388                         contactNumbers, index, messages = self._contacts[0]
389                         if messages:
390                                 self._messagesView.show()
391                                 for message in messages:
392                                         row = (message, )
393                                         self._messagemodel.append(row)
394                                 messagesSelection = self._messagesView.get_selection()
395                                 messagesSelection.select_path((len(messages)-1, ))
396                         else:
397                                 self._messagesView.hide()
398                         self._targetList.hide()
399                         self._phoneButton.show()
400                         self._phoneButton.set_label(contactNumbers[index][1])
401                         if 1 < len(contactNumbers):
402                                 self._phoneButton.set_sensitive(True)
403                         else:
404                                 self._phoneButton.set_sensitive(False)
405                 else:
406                         self._messagesView.hide()
407                         self._targetList.show()
408                         self._phoneButton.hide()
409                         self._phoneButton.set_label("Error: You shouldn't see this")
410
411         def _update_button_state(self):
412                 if len(self._contacts) == 0:
413                         self._dialButton.set_sensitive(False)
414                         self._smsButton.set_sensitive(False)
415                 elif len(self._contacts) == 1:
416                         entryLength = self._smsEntry.get_buffer().get_char_count()
417                         if entryLength == 0:
418                                 self._dialButton.set_sensitive(True)
419                                 self._smsButton.set_sensitive(False)
420                         else:
421                                 self._dialButton.set_sensitive(False)
422                                 self._smsButton.set_sensitive(True)
423                 else:
424                         self._dialButton.set_sensitive(False)
425                         self._smsButton.set_sensitive(True)
426
427         def _to_contact_numbers(self, contactDetails):
428                 for phoneType, phoneNumber in contactDetails:
429                         display = " - ".join((make_pretty(phoneNumber), phoneType))
430                         yield (phoneNumber, display)
431
432         def _pseudo_destroy(self):
433                 self.clear()
434                 self._window.hide()
435
436         def _request_number(self, contactIndex):
437                 contactNumbers, index, messages = self._contacts[contactIndex]
438                 assert 0 <= index, "%r" % index
439
440                 index = hildonize.touch_selector(
441                         self._window,
442                         "Phone Numbers",
443                         (description for (number, description) in contactNumbers),
444                         index,
445                 )
446                 self._contacts[contactIndex] = contactNumbers, index, messages
447
448         def send_sms(self, numbers, message):
449                 raise NotImplementedError()
450
451         def dial(self, number):
452                 raise NotImplementedError()
453
454         def _on_phone(self, *args):
455                 try:
456                         assert len(self._contacts) == 1, "One and only one contact is required"
457                         self._request_number(0)
458
459                         contactNumbers, numberIndex, messages = self._contacts[0]
460                         self._phoneButton.set_label(contactNumbers[numberIndex][1])
461                         row = list(self._targetList.get_children())[0]
462                         phoneButton = list(row.get_children())[1]
463                         phoneButton.set_label(contactNumbers[numberIndex][1])
464                 except Exception, e:
465                         self._errorDisplay.push_exception()
466
467         def _on_choose_phone_n(self, button, row):
468                 try:
469                         assert 1 < len(self._contacts), "More than one contact required"
470                         targetList = list(self._targetList.get_children())
471                         index = targetList.index(row)
472                         self._request_number(index)
473
474                         contactNumbers, numberIndex, messages = self._contacts[0]
475                         phoneButton = list(row.get_children())[1]
476                         phoneButton.set_label(contactNumbers[numberIndex][1])
477                 except Exception, e:
478                         self._errorDisplay.push_exception()
479
480         def _on_remove_phone_n(self, button, row):
481                 try:
482                         assert 1 < len(self._contacts), "More than one contact required"
483                         targetList = list(self._targetList.get_children())
484                         index = targetList.index(row)
485
486                         del self._contacts[index]
487                         self._targetList.remove(row)
488                         self._update_context()
489                         self._update_button_state()
490                 except Exception, e:
491                         self._errorDisplay.push_exception()
492
493         def _on_entry_changed(self, *args):
494                 try:
495                         self._update_letter_count()
496                 except Exception, e:
497                         self._errorDisplay.push_exception()
498
499         def _on_send(self, *args):
500                 try:
501                         assert 0 < len(self._contacts), "At least one contact required (%r)" % self._contacts
502                         phoneNumbers = [
503                                 make_ugly(contact[0][contact[1]][0])
504                                 for contact in self._contacts
505                         ]
506
507                         entryBuffer = self._smsEntry.get_buffer()
508                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
509                         enteredMessage = enteredMessage.strip()
510                         assert enteredMessage, "No message provided"
511                         self.send_sms(phoneNumbers, enteredMessage)
512                         self._pseudo_destroy()
513                 except Exception, e:
514                         self._errorDisplay.push_exception()
515
516         def _on_dial(self, *args):
517                 try:
518                         assert len(self._contacts) == 1, "One and only one contact allowed (%r)" % self._contacts
519                         contact = self._contacts[0]
520                         contactNumber = contact[0][contact[1]][0]
521                         phoneNumber = make_ugly(contactNumber)
522                         self.dial(phoneNumber)
523                         self._pseudo_destroy()
524                 except Exception, e:
525                         self._errorDisplay.push_exception()
526
527         def _on_delete(self, *args):
528                 try:
529                         self._window.emit_stop_by_name("delete-event")
530                         if hildonize.IS_FREMANTLE_SUPPORTED:
531                                 self._window.hide()
532                         else:
533                                 self._pseudo_destroy()
534                 except Exception, e:
535                         self._errorDisplay.push_exception()
536                 return True
537
538         def _on_window_state_change(self, widget, event, *args):
539                 try:
540                         if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
541                                 self._isFullScreen = True
542                         else:
543                                 self._isFullScreen = False
544                 except Exception, e:
545                         self._errorDisplay.push_exception()
546
547         def _on_key_press(self, widget, event):
548                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
549                 try:
550                         if (
551                                 event.keyval == gtk.keysyms.F6 or
552                                 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
553                         ):
554                                 if self._isFullScreen:
555                                         self._window.unfullscreen()
556                                 else:
557                                         self._window.fullscreen()
558                         elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
559                                 message = "\n".join(
560                                         messagePart[0]
561                                         for messagePart in self._messagemodel
562                                 )
563                                 self._clipboard.set_text(str(message))
564                         elif (
565                                 event.keyval == gtk.keysyms.h and
566                                 event.get_state() & gtk.gdk.CONTROL_MASK
567                         ):
568                                 self._window.hide()
569                         elif (
570                                 event.keyval == gtk.keysyms.w and
571                                 event.get_state() & gtk.gdk.CONTROL_MASK
572                         ):
573                                 self._pseudo_destroy()
574                         elif (
575                                 event.keyval == gtk.keysyms.q and
576                                 event.get_state() & gtk.gdk.CONTROL_MASK
577                         ):
578                                 self._parent.destroy()
579                 except Exception, e:
580                         self._errorDisplay.push_exception()
581
582
583 class Dialpad(object):
584
585         def __init__(self, widgetTree, errorDisplay):
586                 self._clipboard = gtk.clipboard_get()
587                 self._errorDisplay = errorDisplay
588
589                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
590                 self._okButton = widgetTree.get_widget("dialpadOk")
591                 self._backButton = widgetTree.get_widget("back")
592                 self._plusButton = widgetTree.get_widget("plus")
593                 self._phonenumber = ""
594                 self._prettynumber = ""
595
596                 callbackMapping = {
597                         "on_digit_clicked": self._on_digit_clicked,
598                 }
599                 widgetTree.signal_autoconnect(callbackMapping)
600                 self._okButton.connect("clicked", self._on_ok_clicked)
601                 self._plusButton.connect("clicked", self._on_plus)
602
603                 self._originalLabel = self._backButton.get_label()
604                 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
605                 self._backTapHandler.on_tap = self._on_backspace
606                 self._backTapHandler.on_hold = self._on_clearall
607                 self._backTapHandler.on_holding = self._set_clear_button
608                 self._backTapHandler.on_cancel = self._reset_back_button
609
610                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
611                 self._keyPressEventId = 0
612
613         def enable(self):
614                 self._okButton.grab_focus()
615                 self._backTapHandler.enable()
616                 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
617
618         def disable(self):
619                 self._window.disconnect(self._keyPressEventId)
620                 self._keyPressEventId = 0
621                 self._reset_back_button()
622                 self._backTapHandler.disable()
623
624         def add_contact(self, *args, **kwds):
625                 """
626                 @note Actual dial function is patched in later
627                 """
628                 raise NotImplementedError("Horrible unknown error has occurred")
629
630         def get_number(self):
631                 return self._phonenumber
632
633         def set_number(self, number):
634                 """
635                 Set the number to dial
636                 """
637                 try:
638                         self._phonenumber = make_ugly(number)
639                         self._prettynumber = make_pretty(self._phonenumber)
640                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
641                 except TypeError, e:
642                         self._errorDisplay.push_exception()
643
644         def clear(self):
645                 self.set_number("")
646
647         @staticmethod
648         def name():
649                 return "Dialpad"
650
651         def load_settings(self, config, section):
652                 pass
653
654         def save_settings(self, config, section):
655                 """
656                 @note Thread Agnostic
657                 """
658                 pass
659
660         def _on_key_press(self, widget, event):
661                 try:
662                         if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
663                                 contents = self._clipboard.wait_for_text()
664                                 if contents is not None:
665                                         self.set_number(contents)
666                 except Exception, e:
667                         self._errorDisplay.push_exception()
668
669         def _on_ok_clicked(self, widget):
670                 try:
671                         phoneNumber = self.get_number()
672                         self.add_contact(
673                                 "(Dialpad)",
674                                 [("Dialer", phoneNumber)], ()
675                         )
676                         self.set_number("")
677                 except Exception, e:
678                         self._errorDisplay.push_exception()
679
680         def _on_digit_clicked(self, widget):
681                 try:
682                         self.set_number(self._phonenumber + widget.get_name()[-1])
683                 except Exception, e:
684                         self._errorDisplay.push_exception()
685
686         def _on_plus(self, *args):
687                 try:
688                         self.set_number(self._phonenumber + "+")
689                 except Exception, e:
690                         self._errorDisplay.push_exception()
691
692         def _on_backspace(self, taps):
693                 try:
694                         self.set_number(self._phonenumber[:-taps])
695                         self._reset_back_button()
696                 except Exception, e:
697                         self._errorDisplay.push_exception()
698
699         def _on_clearall(self, taps):
700                 try:
701                         self.clear()
702                         self._reset_back_button()
703                 except Exception, e:
704                         self._errorDisplay.push_exception()
705                 return False
706
707         def _set_clear_button(self):
708                 try:
709                         self._backButton.set_label("gtk-clear")
710                 except Exception, e:
711                         self._errorDisplay.push_exception()
712
713         def _reset_back_button(self):
714                 try:
715                         self._backButton.set_label(self._originalLabel)
716                 except Exception, e:
717                         self._errorDisplay.push_exception()
718
719
720 class AccountInfo(object):
721
722         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
723                 self._errorDisplay = errorDisplay
724                 self._backend = backend
725                 self._isPopulated = False
726                 self._alarmHandler = alarmHandler
727                 self._notifyOnMissed = False
728                 self._notifyOnVoicemail = False
729                 self._notifyOnSms = False
730
731                 self._callbackList = []
732                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
733                 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
734                 self._onCallbackSelectChangedId = 0
735
736                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
737                 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
738                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
739                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
740                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
741                 self._onNotifyToggled = 0
742                 self._onMinutesChanged = 0
743                 self._onMissedToggled = 0
744                 self._onVoicemailToggled = 0
745                 self._onSmsToggled = 0
746                 self._applyAlarmTimeoutId = None
747
748                 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
749                 self._callbackNumber = ""
750
751         def enable(self):
752                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
753
754                 self._accountViewNumberDisplay.set_use_markup(True)
755                 self.set_account_number("")
756
757                 del self._callbackList[:]
758                 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
759                 self._set_callback_label("")
760
761                 if self._alarmHandler is not None:
762                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
763                         self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
764                         self._missedCheckbox.set_active(self._notifyOnMissed)
765                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
766                         self._smsCheckbox.set_active(self._notifyOnSms)
767
768                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
769                         self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
770                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
771                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
772                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
773                 else:
774                         self._notifyCheckbox.set_sensitive(False)
775                         self._minutesEntryButton.set_sensitive(False)
776                         self._missedCheckbox.set_sensitive(False)
777                         self._voicemailCheckbox.set_sensitive(False)
778                         self._smsCheckbox.set_sensitive(False)
779
780                 self.update(force=True)
781
782         def disable(self):
783                 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
784                 self._onCallbackSelectChangedId = 0
785                 self._set_callback_label("")
786
787                 if self._alarmHandler is not None:
788                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
789                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
790                         self._missedCheckbox.disconnect(self._onNotifyToggled)
791                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
792                         self._smsCheckbox.disconnect(self._onNotifyToggled)
793                         self._onNotifyToggled = 0
794                         self._onMinutesChanged = 0
795                         self._onMissedToggled = 0
796                         self._onVoicemailToggled = 0
797                         self._onSmsToggled = 0
798                 else:
799                         self._notifyCheckbox.set_sensitive(True)
800                         self._minutesEntryButton.set_sensitive(True)
801                         self._missedCheckbox.set_sensitive(True)
802                         self._voicemailCheckbox.set_sensitive(True)
803                         self._smsCheckbox.set_sensitive(True)
804
805                 self.clear()
806                 del self._callbackList[:]
807
808         def set_account_number(self, number):
809                 """
810                 Displays current account number
811                 """
812                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
813
814         def update(self, force = False):
815                 if not force and self._isPopulated:
816                         return False
817                 self._populate_callback_combo()
818                 self.set_account_number(self._backend.get_account_number())
819                 return True
820
821         def clear(self):
822                 self._set_callback_label("")
823                 self.set_account_number("")
824                 self._isPopulated = False
825
826         def save_everything(self):
827                 raise NotImplementedError
828
829         @staticmethod
830         def name():
831                 return "Account Info"
832
833         def load_settings(self, config, section):
834                 self._callbackNumber = make_ugly(config.get(section, "callback"))
835                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
836                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
837                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
838
839         def save_settings(self, config, section):
840                 """
841                 @note Thread Agnostic
842                 """
843                 config.set(section, "callback", self._callbackNumber)
844                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
845                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
846                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
847
848         def _populate_callback_combo(self):
849                 self._isPopulated = True
850                 del self._callbackList[:]
851                 try:
852                         callbackNumbers = self._backend.get_callback_numbers()
853                 except Exception, e:
854                         self._errorDisplay.push_exception()
855                         self._isPopulated = False
856                         return
857
858                 if len(callbackNumbers) == 0:
859                         callbackNumbers = {"": "No callback numbers available"}
860
861                 for number, description in callbackNumbers.iteritems():
862                         self._callbackList.append((make_pretty(number), description))
863
864                 self._set_callback_number(self._callbackNumber)
865
866         def _set_callback_number(self, number):
867                 try:
868                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
869                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
870                         elif number == self._backend.get_callback_number() and 0 < len(number):
871                                 _moduleLogger.warning(
872                                         "Callback number already is %s" % (
873                                                 self._backend.get_callback_number(),
874                                         ),
875                                 )
876                                 self._set_callback_label(number)
877                         else:
878                                 if number.startswith("1747"): number = "+" + number
879                                 self._backend.set_callback_number(number)
880                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
881                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
882                                 )
883                                 self._callbackNumber = make_ugly(number)
884                                 self._set_callback_label(number)
885                                 _moduleLogger.info(
886                                         "Callback number set to %s" % (
887                                                 self._backend.get_callback_number(),
888                                         ),
889                                 )
890                 except Exception, e:
891                         self._errorDisplay.push_exception()
892
893         def _set_callback_label(self, uglyNumber):
894                 prettyNumber = make_pretty(uglyNumber)
895                 if len(prettyNumber) == 0:
896                         prettyNumber = "No Callback Number"
897                 self._callbackSelectButton.set_label(prettyNumber)
898
899         def _update_alarm_settings(self, recurrence):
900                 try:
901                         isEnabled = self._notifyCheckbox.get_active()
902                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
903                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
904                 finally:
905                         self.save_everything()
906                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
907                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
908
909         def _on_callbackentry_clicked(self, *args):
910                 try:
911                         actualSelection = make_pretty(self._callbackNumber)
912
913                         userOptions = dict(
914                                 (number, "%s (%s)" % (number, description))
915                                 for (number, description) in self._callbackList
916                         )
917                         defaultSelection = userOptions.get(actualSelection, actualSelection)
918
919                         userSelection = hildonize.touch_selector_entry(
920                                 self._window,
921                                 "Callback Number",
922                                 list(userOptions.itervalues()),
923                                 defaultSelection,
924                         )
925                         reversedUserOptions = dict(
926                                 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
927                         )
928                         selectedNumber = reversedUserOptions.get(userSelection, userSelection)
929
930                         number = make_ugly(selectedNumber)
931                         self._set_callback_number(number)
932                 except RuntimeError, e:
933                         _moduleLogger.exception("%s" % str(e))
934                 except Exception, e:
935                         self._errorDisplay.push_exception()
936
937         def _on_notify_toggled(self, *args):
938                 try:
939                         if self._applyAlarmTimeoutId is not None:
940                                 gobject.source_remove(self._applyAlarmTimeoutId)
941                                 self._applyAlarmTimeoutId = None
942                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
943                 except Exception, e:
944                         self._errorDisplay.push_exception()
945
946         def _on_minutes_clicked(self, *args):
947                 recurrenceChoices = [
948                         (1, "1 minute"),
949                         (2, "2 minutes"),
950                         (3, "3 minutes"),
951                         (5, "5 minutes"),
952                         (8, "8 minutes"),
953                         (10, "10 minutes"),
954                         (15, "15 minutes"),
955                         (30, "30 minutes"),
956                         (45, "45 minutes"),
957                         (60, "1 hour"),
958                         (3*60, "3 hours"),
959                         (6*60, "6 hours"),
960                         (12*60, "12 hours"),
961                 ]
962                 try:
963                         actualSelection = self._alarmHandler.recurrence
964
965                         closestSelectionIndex = 0
966                         for i, possible in enumerate(recurrenceChoices):
967                                 if possible[0] <= actualSelection:
968                                         closestSelectionIndex = i
969                         recurrenceIndex = hildonize.touch_selector(
970                                 self._window,
971                                 "Minutes",
972                                 (("%s" % m[1]) for m in recurrenceChoices),
973                                 closestSelectionIndex,
974                         )
975                         recurrence = recurrenceChoices[recurrenceIndex][0]
976
977                         self._update_alarm_settings(recurrence)
978                 except RuntimeError, e:
979                         _moduleLogger.exception("%s" % str(e))
980                 except Exception, e:
981                         self._errorDisplay.push_exception()
982
983         def _on_apply_timeout(self, *args):
984                 try:
985                         self._applyAlarmTimeoutId = None
986
987                         self._update_alarm_settings(self._alarmHandler.recurrence)
988                 except Exception, e:
989                         self._errorDisplay.push_exception()
990                 return False
991
992         def _on_missed_toggled(self, *args):
993                 try:
994                         self._notifyOnMissed = self._missedCheckbox.get_active()
995                         self.save_everything()
996                 except Exception, e:
997                         self._errorDisplay.push_exception()
998
999         def _on_voicemail_toggled(self, *args):
1000                 try:
1001                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
1002                         self.save_everything()
1003                 except Exception, e:
1004                         self._errorDisplay.push_exception()
1005
1006         def _on_sms_toggled(self, *args):
1007                 try:
1008                         self._notifyOnSms = self._smsCheckbox.get_active()
1009                         self.save_everything()
1010                 except Exception, e:
1011                         self._errorDisplay.push_exception()
1012
1013
1014 class CallHistoryView(object):
1015
1016         NUMBER_IDX = 0
1017         DATE_IDX = 1
1018         ACTION_IDX = 2
1019         FROM_IDX = 3
1020         FROM_ID_IDX = 4
1021
1022         HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1023
1024         def __init__(self, widgetTree, backend, errorDisplay):
1025                 self._errorDisplay = errorDisplay
1026                 self._backend = backend
1027
1028                 self._isPopulated = False
1029                 self._historymodel = gtk.ListStore(
1030                         gobject.TYPE_STRING, # number
1031                         gobject.TYPE_STRING, # date
1032                         gobject.TYPE_STRING, # action
1033                         gobject.TYPE_STRING, # from
1034                         gobject.TYPE_STRING, # from id
1035                 )
1036                 self._historymodelfiltered = self._historymodel.filter_new()
1037                 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1038                 self._historyview = widgetTree.get_widget("historyview")
1039                 self._historyviewselection = None
1040                 self._onRecentviewRowActivatedId = 0
1041
1042                 textrenderer = gtk.CellRendererText()
1043                 textrenderer.set_property("yalign", 0)
1044                 self._dateColumn = gtk.TreeViewColumn("Date")
1045                 self._dateColumn.pack_start(textrenderer, expand=True)
1046                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1047
1048                 textrenderer = gtk.CellRendererText()
1049                 textrenderer.set_property("yalign", 0)
1050                 self._actionColumn = gtk.TreeViewColumn("Action")
1051                 self._actionColumn.pack_start(textrenderer, expand=True)
1052                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1053
1054                 textrenderer = gtk.CellRendererText()
1055                 textrenderer.set_property("yalign", 0)
1056                 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1057                 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1058                 self._numberColumn = gtk.TreeViewColumn("Number")
1059                 self._numberColumn.pack_start(textrenderer, expand=True)
1060                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1061
1062                 textrenderer = gtk.CellRendererText()
1063                 textrenderer.set_property("yalign", 0)
1064                 hildonize.set_cell_thumb_selectable(textrenderer)
1065                 self._nameColumn = gtk.TreeViewColumn("From")
1066                 self._nameColumn.pack_start(textrenderer, expand=True)
1067                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1068                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1069
1070                 self._window = gtk_toolbox.find_parent_window(self._historyview)
1071
1072                 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1073                 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1074                 self._selectedFilter = "All"
1075
1076                 self._updateSink = gtk_toolbox.threaded_stage(
1077                         gtk_toolbox.comap(
1078                                 self._idly_populate_historyview,
1079                                 gtk_toolbox.null_sink(),
1080                         )
1081                 )
1082
1083         def enable(self):
1084                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1085                 self._historyFilterSelector.set_label(self._selectedFilter)
1086
1087                 self._historyview.set_model(self._historymodelfiltered)
1088                 self._historyview.set_fixed_height_mode(False)
1089
1090                 self._historyview.append_column(self._dateColumn)
1091                 self._historyview.append_column(self._actionColumn)
1092                 self._historyview.append_column(self._numberColumn)
1093                 self._historyview.append_column(self._nameColumn)
1094                 self._historyviewselection = self._historyview.get_selection()
1095                 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1096
1097                 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1098
1099         def disable(self):
1100                 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1101
1102                 self.clear()
1103
1104                 self._historyview.remove_column(self._dateColumn)
1105                 self._historyview.remove_column(self._actionColumn)
1106                 self._historyview.remove_column(self._nameColumn)
1107                 self._historyview.remove_column(self._numberColumn)
1108                 self._historyview.set_model(None)
1109
1110         def add_contact(self, *args, **kwds):
1111                 """
1112                 @note Actual dial function is patched in later
1113                 """
1114                 raise NotImplementedError("Horrible unknown error has occurred")
1115
1116         def update(self, force = False):
1117                 if not force and self._isPopulated:
1118                         return False
1119                 self._updateSink.send(())
1120                 return True
1121
1122         def clear(self):
1123                 self._isPopulated = False
1124                 self._historymodel.clear()
1125
1126         @staticmethod
1127         def name():
1128                 return "Recent Calls"
1129
1130         def load_settings(self, config, sectionName):
1131                 try:
1132                         self._selectedFilter = config.get(sectionName, "filter")
1133                         if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1134                                 self._messageType = self.HISTORY_ITEM_TYPES[0]
1135                 except ConfigParser.NoOptionError:
1136                         pass
1137
1138         def save_settings(self, config, sectionName):
1139                 """
1140                 @note Thread Agnostic
1141                 """
1142                 config.set(sectionName, "filter", self._selectedFilter)
1143
1144         def _is_history_visible(self, model, iter):
1145                 try:
1146                         action = model.get_value(iter, self.ACTION_IDX)
1147                         if action is None:
1148                                 return False # this seems weird but oh well
1149
1150                         if self._selectedFilter in [action, "All"]:
1151                                 return True
1152                         else:
1153                                 return False
1154                 except Exception, e:
1155                         self._errorDisplay.push_exception()
1156
1157         def _idly_populate_historyview(self):
1158                 with gtk_toolbox.gtk_lock():
1159                         banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1160                 try:
1161                         self._historymodel.clear()
1162                         self._isPopulated = True
1163
1164                         try:
1165                                 historyItems = self._backend.get_recent()
1166                         except Exception, e:
1167                                 self._errorDisplay.push_exception_with_lock()
1168                                 self._isPopulated = False
1169                                 historyItems = []
1170
1171                         historyItems = (
1172                                 gv_backend.decorate_recent(data)
1173                                 for data in gv_backend.sort_messages(historyItems)
1174                         )
1175
1176                         for contactId, personName, phoneNumber, date, action in historyItems:
1177                                 if not personName:
1178                                         personName = "Unknown"
1179                                 date = abbrev_relative_date(date)
1180                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1181                                 prettyNumber = make_pretty(prettyNumber)
1182                                 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1183                                 with gtk_toolbox.gtk_lock():
1184                                         self._historymodel.append(item)
1185                 except Exception, e:
1186                         self._errorDisplay.push_exception_with_lock()
1187                 finally:
1188                         with gtk_toolbox.gtk_lock():
1189                                 hildonize.show_busy_banner_end(banner)
1190
1191                 return False
1192
1193         def _on_history_filter_clicked(self, *args, **kwds):
1194                 try:
1195                         selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1196
1197                         try:
1198                                 newSelectedComboIndex = hildonize.touch_selector(
1199                                         self._window,
1200                                         "History",
1201                                         self.HISTORY_ITEM_TYPES,
1202                                         selectedComboIndex,
1203                                 )
1204                         except RuntimeError:
1205                                 return
1206
1207                         option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1208                         self._selectedFilter = option
1209                         self._historyFilterSelector.set_label(self._selectedFilter)
1210                         self._historymodelfiltered.refilter()
1211                 except Exception, e:
1212                         self._errorDisplay.push_exception()
1213
1214         def _history_summary(self, expectedNumber):
1215                 for number, action, date, whoFrom, whoFromId in self._historymodel:
1216                         if expectedNumber is not None and expectedNumber == number:
1217                                 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1218
1219         def _on_historyview_row_activated(self, treeview, path, view_column):
1220                 try:
1221                         childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1222                         itr = self._historymodel.get_iter(childPath)
1223                         if not itr:
1224                                 return
1225
1226                         prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1227                         number = make_ugly(prettyNumber)
1228                         description = list(self._history_summary(prettyNumber))
1229                         contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1230                         contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1231                         contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1232
1233                         self.add_contact(
1234                                 contactName,
1235                                 contactPhoneNumbers,
1236                                 messages = description,
1237                                 defaultIndex = defaultIndex,
1238                         )
1239                         self._historyviewselection.unselect_all()
1240                 except Exception, e:
1241                         self._errorDisplay.push_exception()
1242
1243
1244 class MessagesView(object):
1245
1246         NUMBER_IDX = 0
1247         DATE_IDX = 1
1248         HEADER_IDX = 2
1249         MESSAGE_IDX = 3
1250         MESSAGES_IDX = 4
1251         FROM_ID_IDX = 5
1252         MESSAGE_DATA_IDX = 6
1253
1254         NO_MESSAGES = "None"
1255         VOICEMAIL_MESSAGES = "Voicemail"
1256         TEXT_MESSAGES = "SMS"
1257         ALL_TYPES = "All Messages"
1258         MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1259
1260         UNREAD_STATUS = "Unread"
1261         UNARCHIVED_STATUS = "Inbox"
1262         ALL_STATUS = "Any"
1263         MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1264
1265         def __init__(self, widgetTree, backend, errorDisplay):
1266                 self._errorDisplay = errorDisplay
1267                 self._backend = backend
1268
1269                 self._isPopulated = False
1270                 self._messagemodel = gtk.ListStore(
1271                         gobject.TYPE_STRING, # number
1272                         gobject.TYPE_STRING, # date
1273                         gobject.TYPE_STRING, # header
1274                         gobject.TYPE_STRING, # message
1275                         object, # messages
1276                         gobject.TYPE_STRING, # from id
1277                         object, # message data
1278                 )
1279                 self._messagemodelfiltered = self._messagemodel.filter_new()
1280                 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1281                 self._messageview = widgetTree.get_widget("messages_view")
1282                 self._messageviewselection = None
1283                 self._onMessageviewRowActivatedId = 0
1284
1285                 self._messageRenderer = gtk.CellRendererText()
1286                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1287                 self._messageRenderer.set_property("wrap-width", 500)
1288                 self._messageColumn = gtk.TreeViewColumn("Messages")
1289                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1290                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1291                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1292
1293                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1294
1295                 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1296                 self._onMessageTypeClickedId = 0
1297                 self._messageType = self.ALL_TYPES
1298                 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1299                 self._onMessageStatusClickedId = 0
1300                 self._messageStatus = self.ALL_STATUS
1301
1302                 self._updateSink = gtk_toolbox.threaded_stage(
1303                         gtk_toolbox.comap(
1304                                 self._idly_populate_messageview,
1305                                 gtk_toolbox.null_sink(),
1306                         )
1307                 )
1308
1309         def enable(self):
1310                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1311                 self._messageview.set_model(self._messagemodelfiltered)
1312                 self._messageview.set_headers_visible(False)
1313                 self._messageview.set_fixed_height_mode(False)
1314
1315                 self._messageview.append_column(self._messageColumn)
1316                 self._messageviewselection = self._messageview.get_selection()
1317                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1318
1319                 self._messageTypeButton.set_label(self._messageType)
1320                 self._messageStatusButton.set_label(self._messageStatus)
1321
1322                 self._onMessageviewRowActivatedId = self._messageview.connect(
1323                         "row-activated", self._on_messageview_row_activated
1324                 )
1325                 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1326                         "clicked", self._on_message_type_clicked
1327                 )
1328                 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1329                         "clicked", self._on_message_status_clicked
1330                 )
1331
1332         def disable(self):
1333                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1334                 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1335                 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1336
1337                 self.clear()
1338
1339                 self._messageview.remove_column(self._messageColumn)
1340                 self._messageview.set_model(None)
1341
1342         def add_contact(self, *args, **kwds):
1343                 """
1344                 @note Actual dial function is patched in later
1345                 """
1346                 raise NotImplementedError("Horrible unknown error has occurred")
1347
1348         def update(self, force = False):
1349                 if not force and self._isPopulated:
1350                         return False
1351                 self._updateSink.send(())
1352                 return True
1353
1354         def clear(self):
1355                 self._isPopulated = False
1356                 self._messagemodel.clear()
1357
1358         @staticmethod
1359         def name():
1360                 return "Messages"
1361
1362         def load_settings(self, config, sectionName):
1363                 try:
1364                         self._messageType = config.get(sectionName, "type")
1365                         if self._messageType not in self.MESSAGE_TYPES:
1366                                 self._messageType = self.ALL_TYPES
1367                         self._messageStatus = config.get(sectionName, "status")
1368                         if self._messageStatus not in self.MESSAGE_STATUSES:
1369                                 self._messageStatus = self.ALL_STATUS
1370                 except ConfigParser.NoOptionError:
1371                         pass
1372
1373         def save_settings(self, config, sectionName):
1374                 """
1375                 @note Thread Agnostic
1376                 """
1377                 config.set(sectionName, "status", self._messageStatus)
1378                 config.set(sectionName, "type", self._messageType)
1379
1380         def _is_message_visible(self, model, iter):
1381                 try:
1382                         message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1383                         if message is None:
1384                                 return False # this seems weird but oh well
1385                         return self._filter_messages(message, self._messageType, self._messageStatus)
1386                 except Exception, e:
1387                         self._errorDisplay.push_exception()
1388
1389         @classmethod
1390         def _filter_messages(cls, message, type, status):
1391                 if type == cls.ALL_TYPES:
1392                         isType = True
1393                 else:
1394                         messageType = message["type"]
1395                         isType = messageType == type
1396
1397                 if status == cls.ALL_STATUS:
1398                         isStatus = True
1399                 else:
1400                         isUnarchived = not message["isArchived"]
1401                         isUnread = not message["isRead"]
1402                         if status == cls.UNREAD_STATUS:
1403                                 isStatus = isUnarchived and isUnread
1404                         elif status == cls.UNARCHIVED_STATUS:
1405                                 isStatus = isUnarchived
1406                         else:
1407                                 assert "Status %s is bad for %r" % (status, message)
1408
1409                 return isType and isStatus
1410
1411         _MIN_MESSAGES_SHOWN = 4
1412
1413         def _idly_populate_messageview(self):
1414                 with gtk_toolbox.gtk_lock():
1415                         banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1416                 try:
1417                         self._messagemodel.clear()
1418                         self._isPopulated = True
1419
1420                         if self._messageType == self.NO_MESSAGES:
1421                                 messageItems = []
1422                         else:
1423                                 try:
1424                                         messageItems = self._backend.get_messages()
1425                                 except Exception, e:
1426                                         self._errorDisplay.push_exception_with_lock()
1427                                         self._isPopulated = False
1428                                         messageItems = []
1429
1430                         messageItems = (
1431                                 (gv_backend.decorate_message(message), message)
1432                                 for message in gv_backend.sort_messages(messageItems)
1433                         )
1434
1435                         for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1436                                 prettyNumber = number[2:] if number.startswith("+1") else number
1437                                 prettyNumber = make_pretty(prettyNumber)
1438
1439                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1440                                 expandedMessages = [firstMessage]
1441                                 expandedMessages.extend(messages)
1442                                 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1443                                         firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1444                                         secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1445                                         collapsedMessages = [firstMessage, secondMessage]
1446                                         collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1447                                 else:
1448                                         collapsedMessages = expandedMessages
1449                                 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1450
1451                                 number = make_ugly(number)
1452
1453                                 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1454                                 with gtk_toolbox.gtk_lock():
1455                                         self._messagemodel.append(row)
1456                 except Exception, e:
1457                         self._errorDisplay.push_exception_with_lock()
1458                 finally:
1459                         with gtk_toolbox.gtk_lock():
1460                                 hildonize.show_busy_banner_end(banner)
1461                                 self._messagemodelfiltered.refilter()
1462
1463                 return False
1464
1465         def _on_messageview_row_activated(self, treeview, path, view_column):
1466                 try:
1467                         childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1468                         itr = self._messagemodel.get_iter(childPath)
1469                         if not itr:
1470                                 return
1471
1472                         number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1473                         description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1474
1475                         contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1476                         header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1477                         contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1478
1479                         self.add_contact(
1480                                 header,
1481                                 contactPhoneNumbers,
1482                                 messages = description,
1483                                 defaultIndex = defaultIndex,
1484                         )
1485                         self._messageviewselection.unselect_all()
1486                 except Exception, e:
1487                         self._errorDisplay.push_exception()
1488
1489         def _on_message_type_clicked(self, *args, **kwds):
1490                 try:
1491                         selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1492
1493                         try:
1494                                 newSelectedIndex = hildonize.touch_selector(
1495                                         self._window,
1496                                         "Message Type",
1497                                         self.MESSAGE_TYPES,
1498                                         selectedIndex,
1499                                 )
1500                         except RuntimeError:
1501                                 return
1502
1503                         if selectedIndex != newSelectedIndex:
1504                                 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1505                                 self._messageTypeButton.set_label(self._messageType)
1506                                 self._messagemodelfiltered.refilter()
1507                 except Exception, e:
1508                         self._errorDisplay.push_exception()
1509
1510         def _on_message_status_clicked(self, *args, **kwds):
1511                 try:
1512                         selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1513
1514                         try:
1515                                 newSelectedIndex = hildonize.touch_selector(
1516                                         self._window,
1517                                         "Message Status",
1518                                         self.MESSAGE_STATUSES,
1519                                         selectedIndex,
1520                                 )
1521                         except RuntimeError:
1522                                 return
1523
1524                         if selectedIndex != newSelectedIndex:
1525                                 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1526                                 self._messageStatusButton.set_label(self._messageStatus)
1527                                 self._messagemodelfiltered.refilter()
1528                 except Exception, e:
1529                         self._errorDisplay.push_exception()
1530
1531
1532 class ContactsView(object):
1533
1534         CONTACT_TYPE_IDX = 0
1535         CONTACT_NAME_IDX = 1
1536         CONTACT_ID_IDX = 2
1537
1538         def __init__(self, widgetTree, backend, errorDisplay):
1539                 self._errorDisplay = errorDisplay
1540                 self._backend = backend
1541
1542                 self._addressBook = None
1543                 self._selectedComboIndex = 0
1544                 self._addressBookFactories = [null_backend.NullAddressBook()]
1545
1546                 self._booksList = []
1547                 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1548
1549                 self._isPopulated = False
1550                 self._contactsmodel = gtk.ListStore(
1551                         gobject.TYPE_STRING, # Contact Type
1552                         gobject.TYPE_STRING, # Contact Name
1553                         gobject.TYPE_STRING, # Contact ID
1554                 )
1555                 self._contactsviewselection = None
1556                 self._contactsview = widgetTree.get_widget("contactsview")
1557
1558                 self._contactColumn = gtk.TreeViewColumn("Contact")
1559                 displayContactSource = False
1560                 if displayContactSource:
1561                         textrenderer = gtk.CellRendererText()
1562                         self._contactColumn.pack_start(textrenderer, expand=False)
1563                         self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1564                 textrenderer = gtk.CellRendererText()
1565                 hildonize.set_cell_thumb_selectable(textrenderer)
1566                 self._contactColumn.pack_start(textrenderer, expand=True)
1567                 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1568                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1569                 self._contactColumn.set_sort_column_id(1)
1570                 self._contactColumn.set_visible(True)
1571
1572                 self._onContactsviewRowActivatedId = 0
1573                 self._onAddressbookButtonChangedId = 0
1574                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1575
1576                 self._updateSink = gtk_toolbox.threaded_stage(
1577                         gtk_toolbox.comap(
1578                                 self._idly_populate_contactsview,
1579                                 gtk_toolbox.null_sink(),
1580                         )
1581                 )
1582
1583         def enable(self):
1584                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1585
1586                 self._contactsview.set_model(self._contactsmodel)
1587                 self._contactsview.set_fixed_height_mode(False)
1588                 self._contactsview.append_column(self._contactColumn)
1589                 self._contactsviewselection = self._contactsview.get_selection()
1590                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1591
1592                 del self._booksList[:]
1593                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1594                         if factoryName and bookName:
1595                                 entryName = "%s: %s" % (factoryName, bookName)
1596                         elif factoryName:
1597                                 entryName = factoryName
1598                         elif bookName:
1599                                 entryName = bookName
1600                         else:
1601                                 entryName = "Bad name (%d)" % factoryId
1602                         row = (str(factoryId), bookId, entryName)
1603                         self._booksList.append(row)
1604
1605                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1606                 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1607
1608                 if len(self._booksList) <= self._selectedComboIndex:
1609                         self._selectedComboIndex = 0
1610                 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1611
1612                 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1613                 selectedBookId = self._booksList[self._selectedComboIndex][1]
1614                 self.open_addressbook(selectedFactoryId, selectedBookId)
1615
1616         def disable(self):
1617                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1618                 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1619
1620                 self.clear()
1621
1622                 self._bookSelectionButton.set_label("")
1623                 self._contactsview.set_model(None)
1624                 self._contactsview.remove_column(self._contactColumn)
1625
1626         def add_contact(self, *args, **kwds):
1627                 """
1628                 @note Actual dial function is patched in later
1629                 """
1630                 raise NotImplementedError("Horrible unknown error has occurred")
1631
1632         def get_addressbooks(self):
1633                 """
1634                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1635                 """
1636                 for i, factory in enumerate(self._addressBookFactories):
1637                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1638                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1639
1640         def open_addressbook(self, bookFactoryId, bookId):
1641                 bookFactoryIndex = int(bookFactoryId)
1642                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1643                 self._addressBook = addressBook
1644
1645         def update(self, force = False):
1646                 if not force and self._isPopulated:
1647                         return False
1648                 self._updateSink.send(())
1649                 return True
1650
1651         def clear(self):
1652                 self._isPopulated = False
1653                 self._contactsmodel.clear()
1654                 for factory in self._addressBookFactories:
1655                         factory.clear_caches()
1656                 self._addressBook.clear_caches()
1657
1658         def append(self, book):
1659                 self._addressBookFactories.append(book)
1660
1661         def extend(self, books):
1662                 self._addressBookFactories.extend(books)
1663
1664         @staticmethod
1665         def name():
1666                 return "Contacts"
1667
1668         def load_settings(self, config, sectionName):
1669                 try:
1670                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1671                 except ConfigParser.NoOptionError:
1672                         self._selectedComboIndex = 0
1673
1674         def save_settings(self, config, sectionName):
1675                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1676
1677         def _idly_populate_contactsview(self):
1678                 with gtk_toolbox.gtk_lock():
1679                         banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1680                 try:
1681                         addressBook = None
1682                         while addressBook is not self._addressBook:
1683                                 addressBook = self._addressBook
1684                                 with gtk_toolbox.gtk_lock():
1685                                         self._contactsview.set_model(None)
1686                                         self.clear()
1687
1688                                 try:
1689                                         contacts = addressBook.get_contacts()
1690                                 except Exception, e:
1691                                         contacts = []
1692                                         self._isPopulated = False
1693                                         self._errorDisplay.push_exception_with_lock()
1694                                 for contactId, contactName in contacts:
1695                                         contactType = addressBook.contact_source_short_name(contactId)
1696                                         row = contactType, contactName, contactId
1697                                         self._contactsmodel.append(row)
1698
1699                                 with gtk_toolbox.gtk_lock():
1700                                         self._contactsview.set_model(self._contactsmodel)
1701
1702                         self._isPopulated = True
1703                 except Exception, e:
1704                         self._errorDisplay.push_exception_with_lock()
1705                 finally:
1706                         with gtk_toolbox.gtk_lock():
1707                                 hildonize.show_busy_banner_end(banner)
1708                 return False
1709
1710         def _on_addressbook_button_changed(self, *args, **kwds):
1711                 try:
1712                         try:
1713                                 newSelectedComboIndex = hildonize.touch_selector(
1714                                         self._window,
1715                                         "Addressbook",
1716                                         (("%s" % m[2]) for m in self._booksList),
1717                                         self._selectedComboIndex,
1718                                 )
1719                         except RuntimeError:
1720                                 return
1721
1722                         selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1723                         selectedBookId = self._booksList[newSelectedComboIndex][1]
1724
1725                         oldAddressbook = self._addressBook
1726                         self.open_addressbook(selectedFactoryId, selectedBookId)
1727                         forceUpdate = True if oldAddressbook is not self._addressBook else False
1728                         self.update(force=forceUpdate)
1729
1730                         self._selectedComboIndex = newSelectedComboIndex
1731                         self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1732                 except Exception, e:
1733                         self._errorDisplay.push_exception()
1734
1735         def _on_contactsview_row_activated(self, treeview, path, view_column):
1736                 try:
1737                         itr = self._contactsmodel.get_iter(path)
1738                         if not itr:
1739                                 return
1740
1741                         contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1742                         contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1743                         try:
1744                                 contactDetails = self._addressBook.get_contact_details(contactId)
1745                         except Exception, e:
1746                                 contactDetails = []
1747                                 self._errorDisplay.push_exception()
1748                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1749
1750                         if len(contactPhoneNumbers) == 0:
1751                                 return
1752
1753                         self.add_contact(
1754                                 contactName,
1755                                 contactPhoneNumbers,
1756                                 messages = (contactName, ),
1757                         )
1758                         self._contactsviewselection.unselect_all()
1759                 except Exception, e:
1760                         self._errorDisplay.push_exception()