Fixing the lists on fremantle
[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._defaultCallback = ""
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
698                 if self._alarmHandler is not None:
699                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
700                         self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
701                         self._missedCheckbox.set_active(self._notifyOnMissed)
702                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
703                         self._smsCheckbox.set_active(self._notifyOnSms)
704
705                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
706                         self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
707                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
708                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
709                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
710                 else:
711                         self._notifyCheckbox.set_sensitive(False)
712                         self._minutesEntryButton.set_sensitive(False)
713                         self._missedCheckbox.set_sensitive(False)
714                         self._voicemailCheckbox.set_sensitive(False)
715                         self._smsCheckbox.set_sensitive(False)
716
717                 self.update(force=True)
718
719         def disable(self):
720                 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
721                 self._onCallbackSelectChangedId = 0
722
723                 if self._alarmHandler is not None:
724                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
725                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
726                         self._missedCheckbox.disconnect(self._onNotifyToggled)
727                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
728                         self._smsCheckbox.disconnect(self._onNotifyToggled)
729                         self._onNotifyToggled = 0
730                         self._onMinutesChanged = 0
731                         self._onMissedToggled = 0
732                         self._onVoicemailToggled = 0
733                         self._onSmsToggled = 0
734                 else:
735                         self._notifyCheckbox.set_sensitive(True)
736                         self._minutesEntryButton.set_sensitive(True)
737                         self._missedCheckbox.set_sensitive(True)
738                         self._voicemailCheckbox.set_sensitive(True)
739                         self._smsCheckbox.set_sensitive(True)
740
741                 self.clear()
742                 del self._callbackList[:]
743
744         def get_selected_callback_number(self):
745                 currentLabel = self._callbackSelectButton.get_label()
746                 if currentLabel is not None:
747                         return make_ugly(currentLabel)
748                 else:
749                         return ""
750
751         def set_account_number(self, number):
752                 """
753                 Displays current account number
754                 """
755                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
756
757         def update(self, force = False):
758                 if not force and self._isPopulated:
759                         return False
760                 self._populate_callback_combo()
761                 self.set_account_number(self._backend.get_account_number())
762                 return True
763
764         def clear(self):
765                 self._set_callback_label("")
766                 self.set_account_number("")
767                 self._isPopulated = False
768
769         def save_everything(self):
770                 raise NotImplementedError
771
772         @staticmethod
773         def name():
774                 return "Account Info"
775
776         def load_settings(self, config, section):
777                 self._defaultCallback = config.get(section, "callback")
778                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
779                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
780                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
781
782         def save_settings(self, config, section):
783                 """
784                 @note Thread Agnostic
785                 """
786                 callback = self.get_selected_callback_number()
787                 config.set(section, "callback", callback)
788                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
789                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
790                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
791
792         def _populate_callback_combo(self):
793                 self._isPopulated = True
794                 del self._callbackList[:]
795                 try:
796                         callbackNumbers = self._backend.get_callback_numbers()
797                 except Exception, e:
798                         self._errorDisplay.push_exception()
799                         self._isPopulated = False
800                         return
801
802                 if len(callbackNumbers) == 0:
803                         callbackNumbers = {"": "No callback numbers available"}
804
805                 for number, description in callbackNumbers.iteritems():
806                         self._callbackList.append((make_pretty(number), description))
807
808                 self._set_callback_number(self._defaultCallback)
809
810         def _set_callback_number(self, number):
811                 try:
812                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
813                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
814                         elif number == self._backend.get_callback_number() and 0 < len(number):
815                                 _moduleLogger.warning(
816                                         "Callback number already is %s" % (
817                                                 self._backend.get_callback_number(),
818                                         ),
819                                 )
820                                 self._set_callback_label(number)
821                         else:
822                                 self._backend.set_callback_number(number)
823                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
824                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
825                                 )
826                                 self._set_callback_label(number)
827                                 _moduleLogger.info(
828                                         "Callback number set to %s" % (
829                                                 self._backend.get_callback_number(),
830                                         ),
831                                 )
832                 except Exception, e:
833                         self._errorDisplay.push_exception()
834
835         def _set_callback_label(self, uglyNumber):
836                 prettyNumber = make_pretty(uglyNumber)
837                 if len(prettyNumber) == 0:
838                         prettyNumber = "No Callback Number"
839                 self._callbackSelectButton.set_label(prettyNumber)
840
841         def _update_alarm_settings(self, recurrence):
842                 try:
843                         isEnabled = self._notifyCheckbox.get_active()
844                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
845                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
846                 finally:
847                         self.save_everything()
848                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
849                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
850
851         def _on_callbackentry_clicked(self, *args):
852                 try:
853                         actualSelection = make_pretty(self.get_selected_callback_number())
854
855                         userOptions = dict(
856                                 (number, "%s (%s)" % (number, description))
857                                 for (number, description) in self._callbackList
858                         )
859                         defaultSelection = userOptions.get(actualSelection, actualSelection)
860
861                         userSelection = hildonize.touch_selector_entry(
862                                 self._window,
863                                 "Callback Number",
864                                 list(userOptions.itervalues()),
865                                 defaultSelection,
866                         )
867                         reversedUserOptions = dict(
868                                 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
869                         )
870                         selectedNumber = reversedUserOptions.get(userSelection, userSelection)
871
872                         number = make_ugly(selectedNumber)
873                         self._set_callback_number(number)
874                 except RuntimeError, e:
875                         _moduleLogger.exception("%s" % str(e))
876                 except Exception, e:
877                         self._errorDisplay.push_exception()
878
879         def _on_notify_toggled(self, *args):
880                 try:
881                         if self._applyAlarmTimeoutId is not None:
882                                 gobject.source_remove(self._applyAlarmTimeoutId)
883                                 self._applyAlarmTimeoutId = None
884                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
885                 except Exception, e:
886                         self._errorDisplay.push_exception()
887
888         def _on_minutes_clicked(self, *args):
889                 recurrenceChoices = [
890                         (1, "1 minute"),
891                         (2, "2 minutes"),
892                         (3, "3 minutes"),
893                         (5, "5 minutes"),
894                         (8, "8 minutes"),
895                         (10, "10 minutes"),
896                         (15, "15 minutes"),
897                         (30, "30 minutes"),
898                         (45, "45 minutes"),
899                         (60, "1 hour"),
900                         (3*60, "3 hours"),
901                         (6*60, "6 hours"),
902                         (12*60, "12 hours"),
903                 ]
904                 try:
905                         actualSelection = self._alarmHandler.recurrence
906
907                         closestSelectionIndex = 0
908                         for i, possible in enumerate(recurrenceChoices):
909                                 if possible[0] <= actualSelection:
910                                         closestSelectionIndex = i
911                         recurrenceIndex = hildonize.touch_selector(
912                                 self._window,
913                                 "Minutes",
914                                 (("%s" % m[1]) for m in recurrenceChoices),
915                                 closestSelectionIndex,
916                         )
917                         recurrence = recurrenceChoices[recurrenceIndex][0]
918
919                         self._update_alarm_settings(recurrence)
920                 except RuntimeError, e:
921                         _moduleLogger.exception("%s" % str(e))
922                 except Exception, e:
923                         self._errorDisplay.push_exception()
924
925         def _on_apply_timeout(self, *args):
926                 try:
927                         self._applyAlarmTimeoutId = None
928
929                         self._update_alarm_settings(self._alarmHandler.recurrence)
930                 except Exception, e:
931                         self._errorDisplay.push_exception()
932                 return False
933
934         def _on_missed_toggled(self, *args):
935                 try:
936                         self._notifyOnMissed = self._missedCheckbox.get_active()
937                         self.save_everything()
938                 except Exception, e:
939                         self._errorDisplay.push_exception()
940
941         def _on_voicemail_toggled(self, *args):
942                 try:
943                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
944                         self.save_everything()
945                 except Exception, e:
946                         self._errorDisplay.push_exception()
947
948         def _on_sms_toggled(self, *args):
949                 try:
950                         self._notifyOnSms = self._smsCheckbox.get_active()
951                         self.save_everything()
952                 except Exception, e:
953                         self._errorDisplay.push_exception()
954
955
956 class RecentCallsView(object):
957
958         NUMBER_IDX = 0
959         DATE_IDX = 1
960         ACTION_IDX = 2
961         FROM_IDX = 3
962         FROM_ID_IDX = 4
963
964         def __init__(self, widgetTree, backend, errorDisplay):
965                 self._errorDisplay = errorDisplay
966                 self._backend = backend
967
968                 self._isPopulated = False
969                 self._recentmodel = gtk.ListStore(
970                         gobject.TYPE_STRING, # number
971                         gobject.TYPE_STRING, # date
972                         gobject.TYPE_STRING, # action
973                         gobject.TYPE_STRING, # from
974                         gobject.TYPE_STRING, # from id
975                 )
976                 self._recentview = widgetTree.get_widget("recentview")
977                 self._recentviewselection = None
978                 self._onRecentviewRowActivatedId = 0
979
980                 textrenderer = gtk.CellRendererText()
981                 textrenderer.set_property("yalign", 0)
982                 self._dateColumn = gtk.TreeViewColumn("Date")
983                 self._dateColumn.pack_start(textrenderer, expand=True)
984                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
985
986                 textrenderer = gtk.CellRendererText()
987                 textrenderer.set_property("yalign", 0)
988                 self._actionColumn = gtk.TreeViewColumn("Action")
989                 self._actionColumn.pack_start(textrenderer, expand=True)
990                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
991
992                 textrenderer = gtk.CellRendererText()
993                 textrenderer.set_property("yalign", 0)
994                 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
995                 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
996                 self._numberColumn = gtk.TreeViewColumn("Number")
997                 self._numberColumn.pack_start(textrenderer, expand=True)
998                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
999
1000                 textrenderer = gtk.CellRendererText()
1001                 textrenderer.set_property("yalign", 0)
1002                 hildonize.set_cell_thumb_selectable(textrenderer)
1003                 self._nameColumn = gtk.TreeViewColumn("From")
1004                 self._nameColumn.pack_start(textrenderer, expand=True)
1005                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1006                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1007
1008                 self._window = gtk_toolbox.find_parent_window(self._recentview)
1009                 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1010
1011                 self._updateSink = gtk_toolbox.threaded_stage(
1012                         gtk_toolbox.comap(
1013                                 self._idly_populate_recentview,
1014                                 gtk_toolbox.null_sink(),
1015                         )
1016                 )
1017
1018         def enable(self):
1019                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1020                 self._recentview.set_model(self._recentmodel)
1021                 self._recentview.set_fixed_height_mode(False)
1022
1023                 self._recentview.append_column(self._dateColumn)
1024                 self._recentview.append_column(self._actionColumn)
1025                 self._recentview.append_column(self._numberColumn)
1026                 self._recentview.append_column(self._nameColumn)
1027                 self._recentviewselection = self._recentview.get_selection()
1028                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1029
1030                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1031
1032         def disable(self):
1033                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1034
1035                 self.clear()
1036
1037                 self._recentview.remove_column(self._dateColumn)
1038                 self._recentview.remove_column(self._actionColumn)
1039                 self._recentview.remove_column(self._nameColumn)
1040                 self._recentview.remove_column(self._numberColumn)
1041                 self._recentview.set_model(None)
1042
1043         def number_selected(self, action, number, message):
1044                 """
1045                 @note Actual dial function is patched in later
1046                 """
1047                 raise NotImplementedError("Horrible unknown error has occurred")
1048
1049         def update(self, force = False):
1050                 if not force and self._isPopulated:
1051                         return False
1052                 self._updateSink.send(())
1053                 return True
1054
1055         def clear(self):
1056                 self._isPopulated = False
1057                 self._recentmodel.clear()
1058
1059         @staticmethod
1060         def name():
1061                 return "Recent Calls"
1062
1063         def load_settings(self, config, section):
1064                 pass
1065
1066         def save_settings(self, config, section):
1067                 """
1068                 @note Thread Agnostic
1069                 """
1070                 pass
1071
1072         def _idly_populate_recentview(self):
1073                 with gtk_toolbox.gtk_lock():
1074                         banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1075                 try:
1076                         self._recentmodel.clear()
1077                         self._isPopulated = True
1078
1079                         try:
1080                                 recentItems = self._backend.get_recent()
1081                         except Exception, e:
1082                                 self._errorDisplay.push_exception_with_lock()
1083                                 self._isPopulated = False
1084                                 recentItems = []
1085
1086                         recentItems = (
1087                                 gv_backend.decorate_recent(data)
1088                                 for data in gv_backend.sort_messages(recentItems)
1089                         )
1090
1091                         for contactId, personName, phoneNumber, date, action in recentItems:
1092                                 if not personName:
1093                                         personName = "Unknown"
1094                                 date = abbrev_relative_date(date)
1095                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1096                                 prettyNumber = make_pretty(prettyNumber)
1097                                 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1098                                 with gtk_toolbox.gtk_lock():
1099                                         self._recentmodel.append(item)
1100                 except Exception, e:
1101                         self._errorDisplay.push_exception_with_lock()
1102                 finally:
1103                         with gtk_toolbox.gtk_lock():
1104                                 hildonize.show_busy_banner_end(banner)
1105
1106                 return False
1107
1108         def _on_recentview_row_activated(self, treeview, path, view_column):
1109                 try:
1110                         itr = self._recentmodel.get_iter(path)
1111                         if not itr:
1112                                 return
1113
1114                         number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1115                         number = make_ugly(number)
1116                         description = self._recentmodel.get_value(itr, self.FROM_IDX)
1117                         contactId = self._recentmodel.get_value(itr, self.FROM_ID_IDX)
1118                         if contactId:
1119                                 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1120                                 defaultMatches = [
1121                                         (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1122                                         for (numberDescription, contactNumber) in contactPhoneNumbers
1123                                 ]
1124                                 try:
1125                                         defaultIndex = defaultMatches.index(True)
1126                                 except ValueError:
1127                                         contactPhoneNumbers.append(("Other", number))
1128                                         defaultIndex = len(contactPhoneNumbers)-1
1129                                         _moduleLogger.warn(
1130                                                 "Could not find contact %r's number %s among %r" % (
1131                                                         contactId, number, contactPhoneNumbers
1132                                                 )
1133                                         )
1134                         else:
1135                                 contactPhoneNumbers = [("Phone", number)]
1136                                 defaultIndex = -1
1137
1138                         action, phoneNumber, message = self._phoneTypeSelector.run(
1139                                 contactPhoneNumbers,
1140                                 messages = (description, ),
1141                                 parent = self._window,
1142                                 defaultIndex = defaultIndex,
1143                         )
1144                         if action == SmsEntryDialog.ACTION_CANCEL:
1145                                 return
1146                         assert phoneNumber, "A lack of phone number exists"
1147
1148                         self.number_selected(action, phoneNumber, message)
1149                         self._recentviewselection.unselect_all()
1150                 except Exception, e:
1151                         self._errorDisplay.push_exception()
1152
1153
1154 class MessagesView(object):
1155
1156         NUMBER_IDX = 0
1157         DATE_IDX = 1
1158         HEADER_IDX = 2
1159         MESSAGE_IDX = 3
1160         MESSAGES_IDX = 4
1161         FROM_ID_IDX = 5
1162
1163         NO_MESSAGES = "None"
1164         VOICEMAIL_MESSAGES = "Voicemail"
1165         TEXT_MESSAGES = "Texts"
1166         ALL_MESSAGES = "All Messages"
1167         MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_MESSAGES]
1168
1169         UNREAD_STATUS = "Unread"
1170         UNARCHIVED_STATUS = "Unarchived"
1171         ALL_STATUS = "Any"
1172         MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1173
1174         def __init__(self, widgetTree, backend, errorDisplay):
1175                 self._errorDisplay = errorDisplay
1176                 self._backend = backend
1177
1178                 self._isPopulated = False
1179                 self._messagemodel = gtk.ListStore(
1180                         gobject.TYPE_STRING, # number
1181                         gobject.TYPE_STRING, # date
1182                         gobject.TYPE_STRING, # header
1183                         gobject.TYPE_STRING, # message
1184                         object, # messages
1185                         gobject.TYPE_STRING, # from id
1186                 )
1187                 self._messageview = widgetTree.get_widget("messages_view")
1188                 self._messageviewselection = None
1189                 self._onMessageviewRowActivatedId = 0
1190
1191                 self._messageRenderer = gtk.CellRendererText()
1192                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1193                 self._messageRenderer.set_property("wrap-width", 500)
1194                 self._messageColumn = gtk.TreeViewColumn("Messages")
1195                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1196                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1197                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1198
1199                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1200                 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1201
1202                 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1203                 self._onMessageTypeClickedId = 0
1204                 self._messageType = self.ALL_MESSAGES
1205                 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1206                 self._onMessageStatusClickedId = 0
1207                 self._messageStatus = self.ALL_STATUS
1208
1209                 self._updateSink = gtk_toolbox.threaded_stage(
1210                         gtk_toolbox.comap(
1211                                 self._idly_populate_messageview,
1212                                 gtk_toolbox.null_sink(),
1213                         )
1214                 )
1215
1216         def enable(self):
1217                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1218                 self._messageview.set_model(self._messagemodel)
1219                 self._messageview.set_headers_visible(False)
1220                 self._messageview.set_fixed_height_mode(False)
1221
1222                 self._messageview.append_column(self._messageColumn)
1223                 self._messageviewselection = self._messageview.get_selection()
1224                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1225
1226                 self._messageTypeButton.set_label(self._messageType)
1227                 self._messageStatusButton.set_label(self._messageStatus)
1228
1229                 self._onMessageviewRowActivatedId = self._messageview.connect(
1230                         "row-activated", self._on_messageview_row_activated
1231                 )
1232                 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1233                         "clicked", self._on_message_type_clicked
1234                 )
1235                 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1236                         "clicked", self._on_message_status_clicked
1237                 )
1238
1239         def disable(self):
1240                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1241                 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1242                 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1243
1244                 self.clear()
1245
1246                 self._messageview.remove_column(self._messageColumn)
1247                 self._messageview.set_model(None)
1248
1249         def number_selected(self, action, number, message):
1250                 """
1251                 @note Actual dial function is patched in later
1252                 """
1253                 raise NotImplementedError("Horrible unknown error has occurred")
1254
1255         def update(self, force = False):
1256                 if not force and self._isPopulated:
1257                         return False
1258                 self._updateSink.send(())
1259                 return True
1260
1261         def clear(self):
1262                 self._isPopulated = False
1263                 self._messagemodel.clear()
1264
1265         @staticmethod
1266         def name():
1267                 return "Messages"
1268
1269         def load_settings(self, config, sectionName):
1270                 try:
1271                         self._messageStatus = config.get(sectionName, "status")
1272                         self._messageType = config.get(sectionName, "type")
1273                 except ConfigParser.NoOptionError:
1274                         pass
1275
1276         def save_settings(self, config, sectionName):
1277                 """
1278                 @note Thread Agnostic
1279                 """
1280                 config.set(sectionName, "status", self._messageStatus)
1281                 config.set(sectionName, "type", self._messageType)
1282
1283         _MIN_MESSAGES_SHOWN = 4
1284
1285         @classmethod
1286         def _filter_messages(cls, message, type, status):
1287                 if type == cls.ALL_MESSAGES:
1288                         isType = True
1289                 else:
1290                         messageType = message["type"]
1291                         isType = messageType == type
1292
1293                 if status == cls.ALL_STATUS:
1294                         isStatus = True
1295                 else:
1296                         isUnarchived = not message["isTrash"]
1297                         isUnread = not message["isRead"]
1298                         if status == cls.UNREAD_STATUS:
1299                                 isStatus = isUnarchived and isUnread
1300                         elif status == cls.UNARCHIVED_STATUS:
1301                                 isStatus = isUnarchived
1302                         else:
1303                                 assert "Status %s is bad for %r" % (status, message)
1304
1305                 return isType and isStatus
1306
1307         def _idly_populate_messageview(self):
1308                 with gtk_toolbox.gtk_lock():
1309                         banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1310                 try:
1311                         self._messagemodel.clear()
1312                         self._isPopulated = True
1313
1314                         if self._messageType == self.NO_MESSAGES:
1315                                 messageItems = []
1316                         else:
1317                                 try:
1318                                         messageItems = self._backend.get_messages()
1319                                 except Exception, e:
1320                                         self._errorDisplay.push_exception_with_lock()
1321                                         self._isPopulated = False
1322                                         messageItems = []
1323
1324                         messageItems = (
1325                                 gv_backend.decorate_message(message)
1326                                 for message in gv_backend.sort_messages(messageItems)
1327                                 if self._filter_messages(message, self._messageType, self._messageStatus)
1328                         )
1329
1330                         for contactId, header, number, relativeDate, messages in messageItems:
1331                                 prettyNumber = number[2:] if number.startswith("+1") else number
1332                                 prettyNumber = make_pretty(prettyNumber)
1333
1334                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1335                                 expandedMessages = [firstMessage]
1336                                 expandedMessages.extend(messages)
1337                                 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1338                                         firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1339                                         secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1340                                         collapsedMessages = [firstMessage, secondMessage]
1341                                         collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1342                                 else:
1343                                         collapsedMessages = expandedMessages
1344                                 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1345
1346                                 number = make_ugly(number)
1347
1348                                 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId
1349                                 with gtk_toolbox.gtk_lock():
1350                                         self._messagemodel.append(row)
1351                 except Exception, e:
1352                         self._errorDisplay.push_exception_with_lock()
1353                 finally:
1354                         with gtk_toolbox.gtk_lock():
1355                                 hildonize.show_busy_banner_end(banner)
1356
1357                 return False
1358
1359         def _on_messageview_row_activated(self, treeview, path, view_column):
1360                 try:
1361                         itr = self._messagemodel.get_iter(path)
1362                         if not itr:
1363                                 return
1364
1365                         number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1366                         description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1367
1368                         contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1369                         if contactId:
1370                                 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1371                                 defaultMatches = [
1372                                         (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1373                                         for (numberDescription, contactNumber) in contactPhoneNumbers
1374                                 ]
1375                                 try:
1376                                         defaultIndex = defaultMatches.index(True)
1377                                 except ValueError:
1378                                         contactPhoneNumbers.append(("Other", number))
1379                                         defaultIndex = len(contactPhoneNumbers)-1
1380                                         _moduleLogger.warn(
1381                                                 "Could not find contact %r's number %s among %r" % (
1382                                                         contactId, number, contactPhoneNumbers
1383                                                 )
1384                                         )
1385                         else:
1386                                 contactPhoneNumbers = [("Phone", number)]
1387                                 defaultIndex = -1
1388
1389                         action, phoneNumber, message = self._phoneTypeSelector.run(
1390                                 contactPhoneNumbers,
1391                                 messages = description,
1392                                 parent = self._window,
1393                                 defaultIndex = defaultIndex,
1394                         )
1395                         if action == SmsEntryDialog.ACTION_CANCEL:
1396                                 return
1397                         assert phoneNumber, "A lock of phone number exists"
1398
1399                         self.number_selected(action, phoneNumber, message)
1400                         self._messageviewselection.unselect_all()
1401                 except Exception, e:
1402                         self._errorDisplay.push_exception()
1403
1404         def _on_message_type_clicked(self, *args, **kwds):
1405                 try:
1406                         selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1407
1408                         try:
1409                                 newSelectedIndex = hildonize.touch_selector(
1410                                         self._window,
1411                                         "Message Type",
1412                                         self.MESSAGE_TYPES,
1413                                         selectedIndex,
1414                                 )
1415                         except RuntimeError:
1416                                 return
1417
1418                         if selectedIndex != newSelectedIndex:
1419                                 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1420                                 self._messageTypeButton.set_label(self._messageType)
1421                                 self.update(True)
1422                 except Exception, e:
1423                         self._errorDisplay.push_exception()
1424
1425         def _on_message_status_clicked(self, *args, **kwds):
1426                 try:
1427                         selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1428
1429                         try:
1430                                 newSelectedIndex = hildonize.touch_selector(
1431                                         self._window,
1432                                         "Message Status",
1433                                         self.MESSAGE_STATUSES,
1434                                         selectedIndex,
1435                                 )
1436                         except RuntimeError:
1437                                 return
1438
1439                         if selectedIndex != newSelectedIndex:
1440                                 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1441                                 self._messageStatusButton.set_label(self._messageStatus)
1442                                 self.update(True)
1443                 except Exception, e:
1444                         self._errorDisplay.push_exception()
1445
1446
1447 class ContactsView(object):
1448
1449         def __init__(self, widgetTree, backend, errorDisplay):
1450                 self._errorDisplay = errorDisplay
1451                 self._backend = backend
1452
1453                 self._addressBook = None
1454                 self._selectedComboIndex = 0
1455                 self._addressBookFactories = [null_backend.NullAddressBook()]
1456
1457                 self._booksList = []
1458                 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1459
1460                 self._isPopulated = False
1461                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1462                 self._contactsviewselection = None
1463                 self._contactsview = widgetTree.get_widget("contactsview")
1464
1465                 self._contactColumn = gtk.TreeViewColumn("Contact")
1466                 displayContactSource = False
1467                 if displayContactSource:
1468                         textrenderer = gtk.CellRendererText()
1469                         self._contactColumn.pack_start(textrenderer, expand=False)
1470                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1471                 textrenderer = gtk.CellRendererText()
1472                 hildonize.set_cell_thumb_selectable(textrenderer)
1473                 self._contactColumn.pack_start(textrenderer, expand=True)
1474                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1475                 textrenderer = gtk.CellRendererText()
1476                 self._contactColumn.pack_start(textrenderer, expand=True)
1477                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1478                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1479                 self._contactColumn.set_sort_column_id(1)
1480                 self._contactColumn.set_visible(True)
1481
1482                 self._onContactsviewRowActivatedId = 0
1483                 self._onAddressbookButtonChangedId = 0
1484                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1485                 self._phoneTypeSelector = SmsEntryDialog(widgetTree)
1486
1487                 self._updateSink = gtk_toolbox.threaded_stage(
1488                         gtk_toolbox.comap(
1489                                 self._idly_populate_contactsview,
1490                                 gtk_toolbox.null_sink(),
1491                         )
1492                 )
1493
1494         def enable(self):
1495                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1496
1497                 self._contactsview.set_model(self._contactsmodel)
1498                 self._contactsview.set_fixed_height_mode(False)
1499                 self._contactsview.append_column(self._contactColumn)
1500                 self._contactsviewselection = self._contactsview.get_selection()
1501                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1502
1503                 del self._booksList[:]
1504                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1505                         if factoryName and bookName:
1506                                 entryName = "%s: %s" % (factoryName, bookName)
1507                         elif factoryName:
1508                                 entryName = factoryName
1509                         elif bookName:
1510                                 entryName = bookName
1511                         else:
1512                                 entryName = "Bad name (%d)" % factoryId
1513                         row = (str(factoryId), bookId, entryName)
1514                         self._booksList.append(row)
1515
1516                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1517                 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1518
1519                 if len(self._booksList) <= self._selectedComboIndex:
1520                         self._selectedComboIndex = 0
1521                 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1522
1523                 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1524                 selectedBookId = self._booksList[self._selectedComboIndex][1]
1525                 self.open_addressbook(selectedFactoryId, selectedBookId)
1526
1527         def disable(self):
1528                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1529                 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1530
1531                 self.clear()
1532
1533                 self._bookSelectionButton.set_label("")
1534                 self._contactsview.set_model(None)
1535                 self._contactsview.remove_column(self._contactColumn)
1536
1537         def number_selected(self, action, number, message):
1538                 """
1539                 @note Actual dial function is patched in later
1540                 """
1541                 raise NotImplementedError("Horrible unknown error has occurred")
1542
1543         def get_addressbooks(self):
1544                 """
1545                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1546                 """
1547                 for i, factory in enumerate(self._addressBookFactories):
1548                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1549                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1550
1551         def open_addressbook(self, bookFactoryId, bookId):
1552                 bookFactoryIndex = int(bookFactoryId)
1553                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1554                 self._addressBook = addressBook
1555
1556         def update(self, force = False):
1557                 if not force and self._isPopulated:
1558                         return False
1559                 self._updateSink.send(())
1560                 return True
1561
1562         def clear(self):
1563                 self._isPopulated = False
1564                 self._contactsmodel.clear()
1565                 for factory in self._addressBookFactories:
1566                         factory.clear_caches()
1567                 self._addressBook.clear_caches()
1568
1569         def append(self, book):
1570                 self._addressBookFactories.append(book)
1571
1572         def extend(self, books):
1573                 self._addressBookFactories.extend(books)
1574
1575         @staticmethod
1576         def name():
1577                 return "Contacts"
1578
1579         def load_settings(self, config, sectionName):
1580                 try:
1581                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1582                 except ConfigParser.NoOptionError:
1583                         self._selectedComboIndex = 0
1584
1585         def save_settings(self, config, sectionName):
1586                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1587
1588         def _idly_populate_contactsview(self):
1589                 with gtk_toolbox.gtk_lock():
1590                         banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1591                 try:
1592                         addressBook = None
1593                         while addressBook is not self._addressBook:
1594                                 addressBook = self._addressBook
1595                                 with gtk_toolbox.gtk_lock():
1596                                         self._contactsview.set_model(None)
1597                                         self.clear()
1598
1599                                 try:
1600                                         contacts = addressBook.get_contacts()
1601                                 except Exception, e:
1602                                         contacts = []
1603                                         self._isPopulated = False
1604                                         self._errorDisplay.push_exception_with_lock()
1605                                 for contactId, contactName in contacts:
1606                                         contactType = (addressBook.contact_source_short_name(contactId), )
1607                                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1608
1609                                 with gtk_toolbox.gtk_lock():
1610                                         self._contactsview.set_model(self._contactsmodel)
1611
1612                         self._isPopulated = True
1613                 except Exception, e:
1614                         self._errorDisplay.push_exception_with_lock()
1615                 finally:
1616                         with gtk_toolbox.gtk_lock():
1617                                 hildonize.show_busy_banner_end(banner)
1618                 return False
1619
1620         def _on_addressbook_button_changed(self, *args, **kwds):
1621                 try:
1622                         try:
1623                                 newSelectedComboIndex = hildonize.touch_selector(
1624                                         self._window,
1625                                         "Addressbook",
1626                                         (("%s" % m[2]) for m in self._booksList),
1627                                         self._selectedComboIndex,
1628                                 )
1629                         except RuntimeError:
1630                                 return
1631
1632                         selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1633                         selectedBookId = self._booksList[newSelectedComboIndex][1]
1634
1635                         oldAddressbook = self._addressBook
1636                         self.open_addressbook(selectedFactoryId, selectedBookId)
1637                         forceUpdate = True if oldAddressbook is not self._addressBook else False
1638                         self.update(force=forceUpdate)
1639
1640                         self._selectedComboIndex = newSelectedComboIndex
1641                         self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1642                 except Exception, e:
1643                         self._errorDisplay.push_exception()
1644
1645         def _on_contactsview_row_activated(self, treeview, path, view_column):
1646                 try:
1647                         itr = self._contactsmodel.get_iter(path)
1648                         if not itr:
1649                                 return
1650
1651                         contactId = self._contactsmodel.get_value(itr, 3)
1652                         contactName = self._contactsmodel.get_value(itr, 1)
1653                         try:
1654                                 contactDetails = self._addressBook.get_contact_details(contactId)
1655                         except Exception, e:
1656                                 contactDetails = []
1657                                 self._errorDisplay.push_exception()
1658                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1659
1660                         if len(contactPhoneNumbers) == 0:
1661                                 return
1662
1663                         action, phoneNumber, message = self._phoneTypeSelector.run(
1664                                 contactPhoneNumbers,
1665                                 messages = (contactName, ),
1666                                 parent = self._window,
1667                         )
1668                         if action == SmsEntryDialog.ACTION_CANCEL:
1669                                 return
1670                         assert phoneNumber, "A lack of phone number exists"
1671
1672                         self.number_selected(action, phoneNumber, message)
1673                         self._contactsviewselection.unselect_all()
1674                 except Exception, e:
1675                         self._errorDisplay.push_exception()