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