Fixing of several diablo bugs
[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 Touch selector for callback number
22 @todo Alternate UI for dialogs (stackables)
23 @todo Switch to a selector with entry for notification time
24 @bug Messages can't refresh
25 """
26
27 from __future__ import with_statement
28
29 import ConfigParser
30 import logging
31
32 import gobject
33 import pango
34 import gtk
35
36 import gtk_toolbox
37 import hildonize
38 import null_backend
39
40
41 def make_ugly(prettynumber):
42         """
43         function to take a phone number and strip out all non-numeric
44         characters
45
46         >>> make_ugly("+012-(345)-678-90")
47         '01234567890'
48         """
49         import re
50         uglynumber = re.sub('\D', '', prettynumber)
51         return uglynumber
52
53
54 def make_pretty(phonenumber):
55         """
56         Function to take a phone number and return the pretty version
57         pretty numbers:
58                 if phonenumber begins with 0:
59                         ...-(...)-...-....
60                 if phonenumber begins with 1: ( for gizmo callback numbers )
61                         1 (...)-...-....
62                 if phonenumber is 13 digits:
63                         (...)-...-....
64                 if phonenumber is 10 digits:
65                         ...-....
66         >>> make_pretty("12")
67         '12'
68         >>> make_pretty("1234567")
69         '123-4567'
70         >>> make_pretty("2345678901")
71         '(234)-567-8901'
72         >>> make_pretty("12345678901")
73         '1 (234)-567-8901'
74         >>> make_pretty("01234567890")
75         '+012-(345)-678-90'
76         """
77         if phonenumber is None or phonenumber is "":
78                 return ""
79
80         phonenumber = make_ugly(phonenumber)
81
82         if len(phonenumber) < 3:
83                 return phonenumber
84
85         if phonenumber[0] == "0":
86                 prettynumber = ""
87                 prettynumber += "+%s" % phonenumber[0:3]
88                 if 3 < len(phonenumber):
89                         prettynumber += "-(%s)" % phonenumber[3:6]
90                         if 6 < len(phonenumber):
91                                 prettynumber += "-%s" % phonenumber[6:9]
92                                 if 9 < len(phonenumber):
93                                         prettynumber += "-%s" % phonenumber[9:]
94                 return prettynumber
95         elif len(phonenumber) <= 7:
96                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
97         elif len(phonenumber) > 8 and phonenumber[0] == "1":
98                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
99         elif len(phonenumber) > 7:
100                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
101         return prettynumber
102
103
104 def abbrev_relative_date(date):
105         """
106         >>> abbrev_relative_date("42 hours ago")
107         '42 h'
108         >>> abbrev_relative_date("2 days ago")
109         '2 d'
110         >>> abbrev_relative_date("4 weeks ago")
111         '4 w'
112         """
113         parts = date.split(" ")
114         return "%s %s" % (parts[0], parts[1][0])
115
116
117 class MergedAddressBook(object):
118         """
119         Merger of all addressbooks
120         """
121
122         def __init__(self, addressbookFactories, sorter = None):
123                 self.__addressbookFactories = addressbookFactories
124                 self.__addressbooks = None
125                 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
126
127         def clear_caches(self):
128                 self.__addressbooks = None
129                 for factory in self.__addressbookFactories:
130                         factory.clear_caches()
131
132         def get_addressbooks(self):
133                 """
134                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
135                 """
136                 yield self, "", ""
137
138         def open_addressbook(self, bookId):
139                 return self
140
141         def contact_source_short_name(self, contactId):
142                 if self.__addressbooks is None:
143                         return ""
144                 bookIndex, originalId = contactId.split("-", 1)
145                 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
146
147         @staticmethod
148         def factory_name():
149                 return "All Contacts"
150
151         def get_contacts(self):
152                 """
153                 @returns Iterable of (contact id, contact name)
154                 """
155                 if self.__addressbooks is None:
156                         self.__addressbooks = list(
157                                 factory.open_addressbook(id)
158                                 for factory in self.__addressbookFactories
159                                 for (f, id, name) in factory.get_addressbooks()
160                         )
161                 contacts = (
162                         ("-".join([str(bookIndex), contactId]), contactName)
163                                 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
164                                         for (contactId, contactName) in addressbook.get_contacts()
165                 )
166                 sortedContacts = self.__sort_contacts(contacts)
167                 return sortedContacts
168
169         def get_contact_details(self, contactId):
170                 """
171                 @returns Iterable of (Phone Type, Phone Number)
172                 """
173                 if self.__addressbooks is None:
174                         return []
175                 bookIndex, originalId = contactId.split("-", 1)
176                 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
177
178         @staticmethod
179         def null_sorter(contacts):
180                 """
181                 Good for speed/low memory
182                 """
183                 return contacts
184
185         @staticmethod
186         def basic_firtname_sorter(contacts):
187                 """
188                 Expects names in "First Last" format
189                 """
190                 contactsWithKey = [
191                         (contactName.rsplit(" ", 1)[0], (contactId, contactName))
192                                 for (contactId, contactName) in contacts
193                 ]
194                 contactsWithKey.sort()
195                 return (contactData for (lastName, contactData) in contactsWithKey)
196
197         @staticmethod
198         def basic_lastname_sorter(contacts):
199                 """
200                 Expects names in "First Last" format
201                 """
202                 contactsWithKey = [
203                         (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
204                                 for (contactId, contactName) in contacts
205                 ]
206                 contactsWithKey.sort()
207                 return (contactData for (lastName, contactData) in contactsWithKey)
208
209         @staticmethod
210         def reversed_firtname_sorter(contacts):
211                 """
212                 Expects names in "Last, First" format
213                 """
214                 contactsWithKey = [
215                         (contactName.split(", ", 1)[-1], (contactId, contactName))
216                                 for (contactId, contactName) in contacts
217                 ]
218                 contactsWithKey.sort()
219                 return (contactData for (lastName, contactData) in contactsWithKey)
220
221         @staticmethod
222         def reversed_lastname_sorter(contacts):
223                 """
224                 Expects names in "Last, First" format
225                 """
226                 contactsWithKey = [
227                         (contactName.split(", ", 1)[0], (contactId, contactName))
228                                 for (contactId, contactName) in contacts
229                 ]
230                 contactsWithKey.sort()
231                 return (contactData for (lastName, contactData) in contactsWithKey)
232
233         @staticmethod
234         def guess_firstname(name):
235                 if ", " in name:
236                         return name.split(", ", 1)[-1]
237                 else:
238                         return name.rsplit(" ", 1)[0]
239
240         @staticmethod
241         def guess_lastname(name):
242                 if ", " in name:
243                         return name.split(", ", 1)[0]
244                 else:
245                         return name.rsplit(" ", 1)[-1]
246
247         @classmethod
248         def advanced_firstname_sorter(cls, contacts):
249                 contactsWithKey = [
250                         (cls.guess_firstname(contactName), (contactId, contactName))
251                                 for (contactId, contactName) in contacts
252                 ]
253                 contactsWithKey.sort()
254                 return (contactData for (lastName, contactData) in contactsWithKey)
255
256         @classmethod
257         def advanced_lastname_sorter(cls, contacts):
258                 contactsWithKey = [
259                         (cls.guess_lastname(contactName), (contactId, contactName))
260                                 for (contactId, contactName) in contacts
261                 ]
262                 contactsWithKey.sort()
263                 return (contactData for (lastName, contactData) in contactsWithKey)
264
265
266 class PhoneTypeSelector(object):
267
268         ACTION_CANCEL = "cancel"
269         ACTION_SELECT = "select"
270         ACTION_DIAL = "dial"
271         ACTION_SEND_SMS = "sms"
272
273         def __init__(self, widgetTree, gcBackend):
274                 self._gcBackend = gcBackend
275                 self._widgetTree = widgetTree
276
277                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
278                 self._smsDialog = SmsEntryDialog(self._widgetTree)
279
280                 self._smsButton = self._widgetTree.get_widget("sms_button")
281                 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
282
283                 self._dialButton = self._widgetTree.get_widget("dial_button")
284                 self._dialButton.connect("clicked", self._on_phonetype_dial)
285
286                 self._selectButton = self._widgetTree.get_widget("select_button")
287                 self._selectButton.connect("clicked", self._on_phonetype_select)
288
289                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
290                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
291
292                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
293                 self._messagesView = self._widgetTree.get_widget("phoneSelectionMessages")
294                 self._scrollWindow = self._messagesView.get_parent()
295
296                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
297                 self._typeviewselection = None
298                 self._typeview = self._widgetTree.get_widget("phonetypes")
299                 self._typeview.connect("row-activated", self._on_phonetype_select)
300
301                 self._action = self.ACTION_CANCEL
302
303         def run(self, contactDetails, messages = (), parent = None):
304                 self._action = self.ACTION_CANCEL
305
306                 # Add the column to the phone selection tree view
307                 self._typemodel.clear()
308                 self._typeview.set_model(self._typemodel)
309
310                 textrenderer = gtk.CellRendererText()
311                 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
312                 self._typeview.append_column(numberColumn)
313
314                 textrenderer = gtk.CellRendererText()
315                 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
316                 self._typeview.append_column(typeColumn)
317
318                 for phoneType, phoneNumber in contactDetails:
319                         display = " - ".join((phoneNumber, phoneType))
320                         display = phoneType
321                         row = (phoneNumber, display)
322                         self._typemodel.append(row)
323
324                 self._typeviewselection = self._typeview.get_selection()
325                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
326                 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
327
328                 # Add the column to the messages tree view
329                 self._messagemodel.clear()
330                 self._messagesView.set_model(self._messagemodel)
331
332                 textrenderer = gtk.CellRendererText()
333                 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
334                 textrenderer.set_property("wrap-width", 450)
335                 messageColumn = gtk.TreeViewColumn("")
336                 messageColumn.pack_start(textrenderer, expand=True)
337                 messageColumn.add_attribute(textrenderer, "markup", 0)
338                 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
339                 self._messagesView.append_column(messageColumn)
340                 self._messagesView.set_headers_visible(False)
341
342                 if messages:
343                         for message in messages:
344                                 row = (message, )
345                                 self._messagemodel.append(row)
346                         self._messagesView.show()
347                         self._scrollWindow.show()
348                         messagesSelection = self._messagesView.get_selection()
349                         messagesSelection.select_path((len(messages)-1, ))
350                 else:
351                         self._messagesView.hide()
352                         self._scrollWindow.hide()
353
354                 if parent is not None:
355                         self._dialog.set_transient_for(parent)
356
357                 try:
358                         self._dialog.show()
359                         if messages:
360                                 self._messagesView.scroll_to_cell((len(messages)-1, ))
361
362                         userResponse = self._dialog.run()
363                 finally:
364                         self._dialog.hide()
365
366                 if userResponse == gtk.RESPONSE_OK:
367                         phoneNumber = self._get_number()
368                         phoneNumber = make_ugly(phoneNumber)
369                 else:
370                         phoneNumber = ""
371                 if not phoneNumber:
372                         self._action = self.ACTION_CANCEL
373
374                 if self._action == self.ACTION_SEND_SMS:
375                         smsMessage = self._smsDialog.run(phoneNumber, messages, parent)
376                         if not smsMessage:
377                                 phoneNumber = ""
378                                 self._action = self.ACTION_CANCEL
379                 else:
380                         smsMessage = ""
381
382                 self._messagesView.remove_column(messageColumn)
383                 self._messagesView.set_model(None)
384
385                 self._typeviewselection.unselect_all()
386                 self._typeview.remove_column(numberColumn)
387                 self._typeview.remove_column(typeColumn)
388                 self._typeview.set_model(None)
389
390                 return self._action, phoneNumber, smsMessage
391
392         def _get_number(self):
393                 model, itr = self._typeviewselection.get_selected()
394                 if not itr:
395                         return ""
396
397                 phoneNumber = self._typemodel.get_value(itr, 0)
398                 return phoneNumber
399
400         def _on_phonetype_dial(self, *args):
401                 self._dialog.response(gtk.RESPONSE_OK)
402                 self._action = self.ACTION_DIAL
403
404         def _on_phonetype_send_sms(self, *args):
405                 self._dialog.response(gtk.RESPONSE_OK)
406                 self._action = self.ACTION_SEND_SMS
407
408         def _on_phonetype_select(self, *args):
409                 self._dialog.response(gtk.RESPONSE_OK)
410                 self._action = self.ACTION_SELECT
411
412         def _on_phonetype_cancel(self, *args):
413                 self._dialog.response(gtk.RESPONSE_CANCEL)
414                 self._action = self.ACTION_CANCEL
415
416
417 class SmsEntryDialog(object):
418         """
419         @todo Add multi-SMS messages like GoogleVoice
420         """
421
422         MAX_CHAR = 160
423
424         def __init__(self, widgetTree):
425                 self._widgetTree = widgetTree
426                 self._dialog = self._widgetTree.get_widget("smsDialog")
427
428                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
429                 self._smsButton.connect("clicked", self._on_send)
430
431                 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
432                 self._cancelButton.connect("clicked", self._on_cancel)
433
434                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
435
436                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
437                 self._messagesView = self._widgetTree.get_widget("smsMessages")
438                 self._scrollWindow = self._messagesView.get_parent()
439
440                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
441                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
442
443         def run(self, number, messages = (), parent = None):
444                 # Add the column to the messages tree view
445                 self._messagemodel.clear()
446                 self._messagesView.set_model(self._messagemodel)
447
448                 textrenderer = gtk.CellRendererText()
449                 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
450                 textrenderer.set_property("wrap-width", 450)
451                 messageColumn = gtk.TreeViewColumn("")
452                 messageColumn.pack_start(textrenderer, expand=True)
453                 messageColumn.add_attribute(textrenderer, "markup", 0)
454                 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
455                 self._messagesView.append_column(messageColumn)
456                 self._messagesView.set_headers_visible(False)
457
458                 if messages:
459                         for message in messages:
460                                 row = (message, )
461                                 self._messagemodel.append(row)
462                         self._messagesView.show()
463                         self._scrollWindow.show()
464                         messagesSelection = self._messagesView.get_selection()
465                         messagesSelection.select_path((len(messages)-1, ))
466                 else:
467                         self._messagesView.hide()
468                         self._scrollWindow.hide()
469
470                 self._smsEntry.get_buffer().set_text("")
471                 self._update_letter_count()
472
473                 if parent is not None:
474                         self._dialog.set_transient_for(parent)
475
476                 try:
477                         self._dialog.show()
478                         if messages:
479                                 self._messagesView.scroll_to_cell((len(messages)-1, ))
480                         self._smsEntry.grab_focus()
481
482                         userResponse = self._dialog.run()
483                 finally:
484                         self._dialog.hide()
485
486                 if userResponse == gtk.RESPONSE_OK:
487                         entryBuffer = self._smsEntry.get_buffer()
488                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
489                         enteredMessage = enteredMessage[0:self.MAX_CHAR]
490                 else:
491                         enteredMessage = ""
492
493                 self._messagesView.remove_column(messageColumn)
494                 self._messagesView.set_model(None)
495
496                 return enteredMessage.strip()
497
498         def _update_letter_count(self, *args):
499                 entryLength = self._smsEntry.get_buffer().get_char_count()
500                 charsLeft = self.MAX_CHAR - entryLength
501                 self._letterCountLabel.set_text(str(charsLeft))
502                 if charsLeft < 0:
503                         self._smsButton.set_sensitive(False)
504                 else:
505                         self._smsButton.set_sensitive(True)
506
507         def _on_entry_changed(self, *args):
508                 self._update_letter_count()
509
510         def _on_send(self, *args):
511                 self._dialog.response(gtk.RESPONSE_OK)
512
513         def _on_cancel(self, *args):
514                 self._dialog.response(gtk.RESPONSE_CANCEL)
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                         action = PhoneTypeSelector.ACTION_SEND_SMS
607                         phoneNumber = self.get_number()
608
609                         message = self._smsDialog.run(phoneNumber, (), self._window)
610                         if not message:
611                                 phoneNumber = ""
612                                 action = PhoneTypeSelector.ACTION_CANCEL
613
614                         if action == PhoneTypeSelector.ACTION_CANCEL:
615                                 return
616                         self.number_selected(action, phoneNumber, message)
617                 except Exception, e:
618                         self._errorDisplay.push_exception()
619
620         def _on_dial_clicked(self, widget):
621                 try:
622                         action = PhoneTypeSelector.ACTION_DIAL
623                         phoneNumber = self.get_number()
624                         message = ""
625                         self.number_selected(action, phoneNumber, message)
626                 except Exception, e:
627                         self._errorDisplay.push_exception()
628
629         def _on_digit_clicked(self, widget):
630                 try:
631                         self.set_number(self._phonenumber + widget.get_name()[-1])
632                 except Exception, e:
633                         self._errorDisplay.push_exception()
634
635         def _on_backspace(self, taps):
636                 try:
637                         self.set_number(self._phonenumber[:-taps])
638                         self._reset_back_button()
639                 except Exception, e:
640                         self._errorDisplay.push_exception()
641
642         def _on_clearall(self, taps):
643                 try:
644                         self.clear()
645                         self._reset_back_button()
646                 except Exception, e:
647                         self._errorDisplay.push_exception()
648                 return False
649
650         def _set_clear_button(self):
651                 try:
652                         self._backButton.set_label("gtk-clear")
653                 except Exception, e:
654                         self._errorDisplay.push_exception()
655
656         def _reset_back_button(self):
657                 try:
658                         self._backButton.set_label(self._originalLabel)
659                 except Exception, e:
660                         self._errorDisplay.push_exception()
661
662
663 class AccountInfo(object):
664
665         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
666                 self._errorDisplay = errorDisplay
667                 self._backend = backend
668                 self._isPopulated = False
669                 self._alarmHandler = alarmHandler
670                 self._notifyOnMissed = False
671                 self._notifyOnVoicemail = False
672                 self._notifyOnSms = False
673
674                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
675                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
676                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
677                 self._onCallbackentryChangedId = 0
678
679                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
680                 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
681                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
682                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
683                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
684                 self._onNotifyToggled = 0
685                 self._onMinutesChanged = 0
686                 self._onMissedToggled = 0
687                 self._onVoicemailToggled = 0
688                 self._onSmsToggled = 0
689                 self._applyAlarmTimeoutId = None
690
691                 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
692                 self._defaultCallback = ""
693
694         def enable(self):
695                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
696
697                 self._accountViewNumberDisplay.set_use_markup(True)
698                 self.set_account_number("")
699
700                 self._callbackList.clear()
701                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
702
703                 if self._alarmHandler is not None:
704                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
705                         self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
706                         self._missedCheckbox.set_active(self._notifyOnMissed)
707                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
708                         self._smsCheckbox.set_active(self._notifyOnSms)
709
710                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
711                         self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
712                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
713                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
714                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
715                 else:
716                         self._notifyCheckbox.set_sensitive(False)
717                         self._minutesEntryButton.set_sensitive(False)
718                         self._missedCheckbox.set_sensitive(False)
719                         self._voicemailCheckbox.set_sensitive(False)
720                         self._smsCheckbox.set_sensitive(False)
721
722                 self.update(force=True)
723
724         def disable(self):
725                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
726                 self._onCallbackentryChangedId = 0
727
728                 if self._alarmHandler is not None:
729                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
730                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
731                         self._missedCheckbox.disconnect(self._onNotifyToggled)
732                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
733                         self._smsCheckbox.disconnect(self._onNotifyToggled)
734                         self._onNotifyToggled = 0
735                         self._onMinutesChanged = 0
736                         self._onMissedToggled = 0
737                         self._onVoicemailToggled = 0
738                         self._onSmsToggled = 0
739                 else:
740                         self._notifyCheckbox.set_sensitive(True)
741                         self._minutesEntryButton.set_sensitive(True)
742                         self._missedCheckbox.set_sensitive(True)
743                         self._voicemailCheckbox.set_sensitive(True)
744                         self._smsCheckbox.set_sensitive(True)
745
746                 self.clear()
747                 self._callbackList.clear()
748
749         def get_selected_callback_number(self):
750                 return make_ugly(self._callbackCombo.get_child().get_text())
751
752         def set_account_number(self, number):
753                 """
754                 Displays current account number
755                 """
756                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
757
758         def update(self, force = False):
759                 if not force and self._isPopulated:
760                         return False
761                 self._populate_callback_combo()
762                 self.set_account_number(self._backend.get_account_number())
763                 return True
764
765         def clear(self):
766                 self._callbackCombo.get_child().set_text("")
767                 self.set_account_number("")
768                 self._isPopulated = False
769
770         def save_everything(self):
771                 raise NotImplementedError
772
773         @staticmethod
774         def name():
775                 return "Account Info"
776
777         def load_settings(self, config, section):
778                 self._defaultCallback = config.get(section, "callback")
779                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
780                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
781                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
782
783         def save_settings(self, config, section):
784                 """
785                 @note Thread Agnostic
786                 """
787                 callback = self.get_selected_callback_number()
788                 config.set(section, "callback", callback)
789                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
790                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
791                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
792
793         def _populate_callback_combo(self):
794                 self._isPopulated = True
795                 self._callbackList.clear()
796                 try:
797                         callbackNumbers = self._backend.get_callback_numbers()
798                 except Exception, e:
799                         self._errorDisplay.push_exception()
800                         self._isPopulated = False
801                         return
802
803                 for number, description in callbackNumbers.iteritems():
804                         self._callbackList.append((make_pretty(number),))
805
806                 self._callbackCombo.set_model(self._callbackList)
807                 self._callbackCombo.set_text_column(0)
808                 #callbackNumber = self._backend.get_callback_number()
809                 callbackNumber = self._defaultCallback
810                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
811
812         def _set_callback_number(self, number):
813                 try:
814                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
815                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
816                         elif number == self._backend.get_callback_number():
817                                 logging.warning(
818                                         "Callback number already is %s" % (
819                                                 self._backend.get_callback_number(),
820                                         ),
821                                 )
822                         else:
823                                 self._backend.set_callback_number(number)
824                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
825                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
826                                 )
827                                 logging.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 _update_alarm_settings(self, recurrence):
836                 try:
837                         isEnabled = self._notifyCheckbox.get_active()
838                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
839                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
840                 finally:
841                         self.save_everything()
842                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
843                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
844
845         def _on_callbackentry_changed(self, *args):
846                 try:
847                         text = self.get_selected_callback_number()
848                         number = make_ugly(text)
849                         self._set_callback_number(number)
850                 except Exception, e:
851                         self._errorDisplay.push_exception()
852
853         def _on_notify_toggled(self, *args):
854                 try:
855                         if self._applyAlarmTimeoutId is not None:
856                                 gobject.source_remove(self._applyAlarmTimeoutId)
857                                 self._applyAlarmTimeoutId = None
858                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
859                 except Exception, e:
860                         self._errorDisplay.push_exception()
861
862         def _on_minutes_clicked(self, *args):
863                 recurrenceChoices = [
864                         (1, "1 minute"),
865                         (2, "2 minutes"),
866                         (3, "3 minutes"),
867                         (5, "5 minutes"),
868                         (8, "8 minutes"),
869                         (10, "10 minutes"),
870                         (15, "15 minutes"),
871                         (30, "30 minutes"),
872                         (45, "45 minutes"),
873                         (60, "1 hour"),
874                         (3*60, "3 hours"),
875                         (6*60, "6 hours"),
876                         (12*60, "12 hours"),
877                 ]
878                 try:
879                         actualSelection = self._alarmHandler.recurrence
880
881                         closestSelectionIndex = 0
882                         for i, possible in enumerate(recurrenceChoices):
883                                 if possible[0] <= actualSelection:
884                                         closestSelectionIndex = i
885                         recurrenceIndex = hildonize.touch_selector(
886                                 self._window,
887                                 "Minutes",
888                                 (("%s" % m[1]) for m in recurrenceChoices),
889                                 closestSelectionIndex,
890                         )
891                         recurrence = recurrenceChoices[recurrenceIndex][0]
892
893                         self._update_alarm_settings(recurrence)
894                 except RuntimeError, e:
895                         logging.exception("%s" % str(e))
896                 except Exception, e:
897                         self._errorDisplay.push_exception()
898
899         def _on_apply_timeout(self, *args):
900                 try:
901                         self._applyAlarmTimeoutId = None
902
903                         self._update_alarm_settings(self._alarmHandler.recurrence)
904                 except Exception, e:
905                         self._errorDisplay.push_exception()
906                 return False
907
908         def _on_missed_toggled(self, *args):
909                 try:
910                         self._notifyOnMissed = self._missedCheckbox.get_active()
911                         self.save_everything()
912                 except Exception, e:
913                         self._errorDisplay.push_exception()
914
915         def _on_voicemail_toggled(self, *args):
916                 try:
917                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
918                         self.save_everything()
919                 except Exception, e:
920                         self._errorDisplay.push_exception()
921
922         def _on_sms_toggled(self, *args):
923                 try:
924                         self._notifyOnSms = self._smsCheckbox.get_active()
925                         self.save_everything()
926                 except Exception, e:
927                         self._errorDisplay.push_exception()
928
929
930 class RecentCallsView(object):
931
932         NUMBER_IDX = 0
933         DATE_IDX = 1
934         ACTION_IDX = 2
935         FROM_IDX = 3
936
937         def __init__(self, widgetTree, backend, errorDisplay):
938                 self._errorDisplay = errorDisplay
939                 self._backend = backend
940
941                 self._isPopulated = False
942                 self._recentmodel = gtk.ListStore(
943                         gobject.TYPE_STRING, # number
944                         gobject.TYPE_STRING, # date
945                         gobject.TYPE_STRING, # action
946                         gobject.TYPE_STRING, # from
947                 )
948                 self._recentview = widgetTree.get_widget("recentview")
949                 self._recentviewselection = None
950                 self._onRecentviewRowActivatedId = 0
951
952                 textrenderer = gtk.CellRendererText()
953                 textrenderer.set_property("yalign", 0)
954                 self._dateColumn = gtk.TreeViewColumn("Date")
955                 self._dateColumn.pack_start(textrenderer, expand=True)
956                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
957
958                 textrenderer = gtk.CellRendererText()
959                 textrenderer.set_property("yalign", 0)
960                 self._actionColumn = gtk.TreeViewColumn("Action")
961                 self._actionColumn.pack_start(textrenderer, expand=True)
962                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
963
964                 textrenderer = gtk.CellRendererText()
965                 textrenderer.set_property("yalign", 0)
966                 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
967                 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
968                 self._numberColumn = gtk.TreeViewColumn("Number")
969                 self._numberColumn.pack_start(textrenderer, expand=True)
970                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
971
972                 textrenderer = gtk.CellRendererText()
973                 textrenderer.set_property("yalign", 0)
974                 hildonize.set_cell_thumb_selectable(textrenderer)
975                 self._nameColumn = gtk.TreeViewColumn("From")
976                 self._nameColumn.pack_start(textrenderer, expand=True)
977                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
978                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
979
980                 self._window = gtk_toolbox.find_parent_window(self._recentview)
981                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
982
983                 self._updateSink = gtk_toolbox.threaded_stage(
984                         gtk_toolbox.comap(
985                                 self._idly_populate_recentview,
986                                 gtk_toolbox.null_sink(),
987                         )
988                 )
989
990         def enable(self):
991                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
992                 self._recentview.set_model(self._recentmodel)
993
994                 self._recentview.append_column(self._dateColumn)
995                 self._recentview.append_column(self._actionColumn)
996                 self._recentview.append_column(self._numberColumn)
997                 self._recentview.append_column(self._nameColumn)
998                 self._recentviewselection = self._recentview.get_selection()
999                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1000
1001                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1002
1003         def disable(self):
1004                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1005
1006                 self.clear()
1007
1008                 self._recentview.remove_column(self._dateColumn)
1009                 self._recentview.remove_column(self._actionColumn)
1010                 self._recentview.remove_column(self._nameColumn)
1011                 self._recentview.remove_column(self._numberColumn)
1012                 self._recentview.set_model(None)
1013
1014         def number_selected(self, action, number, message):
1015                 """
1016                 @note Actual dial function is patched in later
1017                 """
1018                 raise NotImplementedError("Horrible unknown error has occurred")
1019
1020         def update(self, force = False):
1021                 if not force and self._isPopulated:
1022                         return False
1023                 self._updateSink.send(())
1024                 return True
1025
1026         def clear(self):
1027                 self._isPopulated = False
1028                 self._recentmodel.clear()
1029
1030         @staticmethod
1031         def name():
1032                 return "Recent Calls"
1033
1034         def load_settings(self, config, section):
1035                 pass
1036
1037         def save_settings(self, config, section):
1038                 """
1039                 @note Thread Agnostic
1040                 """
1041                 pass
1042
1043         def _idly_populate_recentview(self):
1044                 with gtk_toolbox.gtk_lock():
1045                         banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1046                 try:
1047                         self._recentmodel.clear()
1048                         self._isPopulated = True
1049
1050                         try:
1051                                 recentItems = self._backend.get_recent()
1052                         except Exception, e:
1053                                 self._errorDisplay.push_exception_with_lock()
1054                                 self._isPopulated = False
1055                                 recentItems = []
1056
1057                         for personName, phoneNumber, date, action in recentItems:
1058                                 if not personName:
1059                                         personName = "Unknown"
1060                                 date = abbrev_relative_date(date)
1061                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1062                                 prettyNumber = make_pretty(prettyNumber)
1063                                 item = (prettyNumber, date, action.capitalize(), personName)
1064                                 with gtk_toolbox.gtk_lock():
1065                                         self._recentmodel.append(item)
1066                 except Exception, e:
1067                         self._errorDisplay.push_exception_with_lock()
1068                 finally:
1069                         with gtk_toolbox.gtk_lock():
1070                                 hildonize.show_busy_banner_end(banner)
1071
1072                 return False
1073
1074         def _on_recentview_row_activated(self, treeview, path, view_column):
1075                 try:
1076                         model, itr = self._recentviewselection.get_selected()
1077                         if not itr:
1078                                 return
1079
1080                         number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1081                         number = make_ugly(number)
1082                         contactPhoneNumbers = [("Phone", number)]
1083                         description = self._recentmodel.get_value(itr, self.FROM_IDX)
1084
1085                         action, phoneNumber, message = self._phoneTypeSelector.run(
1086                                 contactPhoneNumbers,
1087                                 messages = (description, ),
1088                                 parent = self._window,
1089                         )
1090                         if action == PhoneTypeSelector.ACTION_CANCEL:
1091                                 return
1092                         assert phoneNumber, "A lack of phone number exists"
1093
1094                         self.number_selected(action, phoneNumber, message)
1095                         self._recentviewselection.unselect_all()
1096                 except Exception, e:
1097                         self._errorDisplay.push_exception()
1098
1099
1100 class MessagesView(object):
1101
1102         NUMBER_IDX = 0
1103         DATE_IDX = 1
1104         HEADER_IDX = 2
1105         MESSAGE_IDX = 3
1106         MESSAGES_IDX = 4
1107
1108         def __init__(self, widgetTree, backend, errorDisplay):
1109                 self._errorDisplay = errorDisplay
1110                 self._backend = backend
1111
1112                 self._isPopulated = False
1113                 self._messagemodel = gtk.ListStore(
1114                         gobject.TYPE_STRING, # number
1115                         gobject.TYPE_STRING, # date
1116                         gobject.TYPE_STRING, # header
1117                         gobject.TYPE_STRING, # message
1118                         object, # messages
1119                 )
1120                 self._messageview = widgetTree.get_widget("messages_view")
1121                 self._messageviewselection = None
1122                 self._onMessageviewRowActivatedId = 0
1123
1124                 self._messageRenderer = gtk.CellRendererText()
1125                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1126                 self._messageRenderer.set_property("wrap-width", 500)
1127                 self._messageColumn = gtk.TreeViewColumn("Messages")
1128                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1129                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1130                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1131
1132                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1133                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1134
1135                 self._updateSink = gtk_toolbox.threaded_stage(
1136                         gtk_toolbox.comap(
1137                                 self._idly_populate_messageview,
1138                                 gtk_toolbox.null_sink(),
1139                         )
1140                 )
1141
1142         def enable(self):
1143                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1144                 self._messageview.set_model(self._messagemodel)
1145
1146                 self._messageview.append_column(self._messageColumn)
1147                 self._messageviewselection = self._messageview.get_selection()
1148                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1149
1150                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1151
1152         def disable(self):
1153                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1154
1155                 self.clear()
1156
1157                 self._messageview.remove_column(self._messageColumn)
1158                 self._messageview.set_model(None)
1159
1160         def number_selected(self, action, number, message):
1161                 """
1162                 @note Actual dial function is patched in later
1163                 """
1164                 raise NotImplementedError("Horrible unknown error has occurred")
1165
1166         def update(self, force = False):
1167                 if not force and self._isPopulated:
1168                         return False
1169                 self._updateSink.send(())
1170                 return True
1171
1172         def clear(self):
1173                 self._isPopulated = False
1174                 self._messagemodel.clear()
1175
1176         @staticmethod
1177         def name():
1178                 return "Messages"
1179
1180         def load_settings(self, config, section):
1181                 pass
1182
1183         def save_settings(self, config, section):
1184                 """
1185                 @note Thread Agnostic
1186                 """
1187                 pass
1188
1189         def _idly_populate_messageview(self):
1190                 with gtk_toolbox.gtk_lock():
1191                         banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1192                 try:
1193                         self._messagemodel.clear()
1194                         self._isPopulated = True
1195
1196                         try:
1197                                 messageItems = self._backend.get_messages()
1198                         except Exception, e:
1199                                 self._errorDisplay.push_exception_with_lock()
1200                                 self._isPopulated = False
1201                                 messageItems = []
1202
1203                         for header, number, relativeDate, messages in messageItems:
1204                                 prettyNumber = number[2:] if number.startswith("+1") else number
1205                                 prettyNumber = make_pretty(prettyNumber)
1206
1207                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1208                                 newMessages = [firstMessage]
1209                                 newMessages.extend(messages)
1210
1211                                 number = make_ugly(number)
1212
1213                                 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1214                                 with gtk_toolbox.gtk_lock():
1215                                         self._messagemodel.append(row)
1216                 except Exception, e:
1217                         self._errorDisplay.push_exception_with_lock()
1218                 finally:
1219                         with gtk_toolbox.gtk_lock():
1220                                 hildonize.show_busy_banner_end(banner)
1221
1222                 return False
1223
1224         def _on_messageview_row_activated(self, treeview, path, view_column):
1225                 try:
1226                         model, itr = self._messageviewselection.get_selected()
1227                         if not itr:
1228                                 return
1229
1230                         contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1231                         description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1232
1233                         action, phoneNumber, message = self._phoneTypeSelector.run(
1234                                 contactPhoneNumbers,
1235                                 messages = description,
1236                                 parent = self._window,
1237                         )
1238                         if action == PhoneTypeSelector.ACTION_CANCEL:
1239                                 return
1240                         assert phoneNumber, "A lock of phone number exists"
1241
1242                         self.number_selected(action, phoneNumber, message)
1243                         self._messageviewselection.unselect_all()
1244                 except Exception, e:
1245                         self._errorDisplay.push_exception()
1246
1247
1248 class ContactsView(object):
1249
1250         def __init__(self, widgetTree, backend, errorDisplay):
1251                 self._errorDisplay = errorDisplay
1252                 self._backend = backend
1253
1254                 self._addressBook = None
1255                 self._selectedComboIndex = 0
1256                 self._addressBookFactories = [null_backend.NullAddressBook()]
1257
1258                 self._booksList = []
1259                 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1260
1261                 self._isPopulated = False
1262                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1263                 self._contactsviewselection = None
1264                 self._contactsview = widgetTree.get_widget("contactsview")
1265
1266                 self._contactColumn = gtk.TreeViewColumn("Contact")
1267                 displayContactSource = False
1268                 if displayContactSource:
1269                         textrenderer = gtk.CellRendererText()
1270                         self._contactColumn.pack_start(textrenderer, expand=False)
1271                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1272                 textrenderer = gtk.CellRendererText()
1273                 hildonize.set_cell_thumb_selectable(textrenderer)
1274                 self._contactColumn.pack_start(textrenderer, expand=True)
1275                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1276                 textrenderer = gtk.CellRendererText()
1277                 self._contactColumn.pack_start(textrenderer, expand=True)
1278                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1279                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1280                 self._contactColumn.set_sort_column_id(1)
1281                 self._contactColumn.set_visible(True)
1282
1283                 self._onContactsviewRowActivatedId = 0
1284                 self._onAddressbookButtonChangedId = 0
1285                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1286                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1287
1288                 self._updateSink = gtk_toolbox.threaded_stage(
1289                         gtk_toolbox.comap(
1290                                 self._idly_populate_contactsview,
1291                                 gtk_toolbox.null_sink(),
1292                         )
1293                 )
1294
1295         def enable(self):
1296                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1297
1298                 self._contactsview.set_model(self._contactsmodel)
1299                 self._contactsview.append_column(self._contactColumn)
1300                 self._contactsviewselection = self._contactsview.get_selection()
1301                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1302
1303                 del self._booksList[:]
1304                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1305                         if factoryName and bookName:
1306                                 entryName = "%s: %s" % (factoryName, bookName)
1307                         elif factoryName:
1308                                 entryName = factoryName
1309                         elif bookName:
1310                                 entryName = bookName
1311                         else:
1312                                 entryName = "Bad name (%d)" % factoryId
1313                         row = (str(factoryId), bookId, entryName)
1314                         self._booksList.append(row)
1315
1316                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1317                 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1318
1319                 if len(self._booksList) <= self._selectedComboIndex:
1320                         self._selectedComboIndex = 0
1321                 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1322
1323                 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1324                 selectedBookId = self._booksList[self._selectedComboIndex][1]
1325                 self.open_addressbook(selectedFactoryId, selectedBookId)
1326
1327         def disable(self):
1328                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1329                 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1330
1331                 self.clear()
1332
1333                 self._bookSelectionButton.set_label("")
1334                 self._contactsview.set_model(None)
1335                 self._contactsview.remove_column(self._contactColumn)
1336
1337         def number_selected(self, action, number, message):
1338                 """
1339                 @note Actual dial function is patched in later
1340                 """
1341                 raise NotImplementedError("Horrible unknown error has occurred")
1342
1343         def get_addressbooks(self):
1344                 """
1345                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1346                 """
1347                 for i, factory in enumerate(self._addressBookFactories):
1348                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1349                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1350
1351         def open_addressbook(self, bookFactoryId, bookId):
1352                 bookFactoryIndex = int(bookFactoryId)
1353                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1354
1355                 forceUpdate = True if addressBook is not self._addressBook else False
1356
1357                 self._addressBook = addressBook
1358                 self.update(force=forceUpdate)
1359
1360         def update(self, force = False):
1361                 if not force and self._isPopulated:
1362                         return False
1363                 self._updateSink.send(())
1364                 return True
1365
1366         def clear(self):
1367                 self._isPopulated = False
1368                 self._contactsmodel.clear()
1369                 for factory in self._addressBookFactories:
1370                         factory.clear_caches()
1371                 self._addressBook.clear_caches()
1372
1373         def append(self, book):
1374                 self._addressBookFactories.append(book)
1375
1376         def extend(self, books):
1377                 self._addressBookFactories.extend(books)
1378
1379         @staticmethod
1380         def name():
1381                 return "Contacts"
1382
1383         def load_settings(self, config, sectionName):
1384                 try:
1385                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1386                 except ConfigParser.NoOptionError:
1387                         self._selectedComboIndex = 0
1388
1389         def save_settings(self, config, sectionName):
1390                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1391
1392         def _idly_populate_contactsview(self):
1393                 with gtk_toolbox.gtk_lock():
1394                         banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1395                 try:
1396                         addressBook = None
1397                         while addressBook is not self._addressBook:
1398                                 addressBook = self._addressBook
1399                                 with gtk_toolbox.gtk_lock():
1400                                         self._contactsview.set_model(None)
1401                                         self.clear()
1402
1403                                 try:
1404                                         contacts = addressBook.get_contacts()
1405                                 except Exception, e:
1406                                         contacts = []
1407                                         self._isPopulated = False
1408                                         self._errorDisplay.push_exception_with_lock()
1409                                 for contactId, contactName in contacts:
1410                                         contactType = (addressBook.contact_source_short_name(contactId), )
1411                                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1412
1413                                 with gtk_toolbox.gtk_lock():
1414                                         self._contactsview.set_model(self._contactsmodel)
1415
1416                         self._isPopulated = True
1417                 except Exception, e:
1418                         self._errorDisplay.push_exception_with_lock()
1419                 finally:
1420                         with gtk_toolbox.gtk_lock():
1421                                 hildonize.show_busy_banner_end(banner)
1422                 return False
1423
1424         def _on_addressbook_button_changed(self, *args, **kwds):
1425                 try:
1426                         try:
1427                                 newSelectedComboIndex = hildonize.touch_selector(
1428                                         self._window,
1429                                         "Addressbook",
1430                                         (("%s" % m[2]) for m in self._booksList),
1431                                         self._selectedComboIndex,
1432                                 )
1433                         except RuntimeError:
1434                                 return
1435
1436                         selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1437                         selectedBookId = self._booksList[newSelectedComboIndex][1]
1438                         self.open_addressbook(selectedFactoryId, selectedBookId)
1439                         self._selectedComboIndex = newSelectedComboIndex
1440                         self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1441                 except Exception, e:
1442                         self._errorDisplay.push_exception()
1443
1444         def _on_contactsview_row_activated(self, treeview, path, view_column):
1445                 try:
1446                         model, itr = self._contactsviewselection.get_selected()
1447                         if not itr:
1448                                 return
1449
1450                         contactId = self._contactsmodel.get_value(itr, 3)
1451                         contactName = self._contactsmodel.get_value(itr, 1)
1452                         try:
1453                                 contactDetails = self._addressBook.get_contact_details(contactId)
1454                         except Exception, e:
1455                                 contactDetails = []
1456                                 self._errorDisplay.push_exception()
1457                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1458
1459                         if len(contactPhoneNumbers) == 0:
1460                                 return
1461
1462                         action, phoneNumber, message = self._phoneTypeSelector.run(
1463                                 contactPhoneNumbers,
1464                                 messages = (contactName, ),
1465                                 parent = self._window,
1466                         )
1467                         if action == PhoneTypeSelector.ACTION_CANCEL:
1468                                 return
1469                         assert phoneNumber, "A lack of phone number exists"
1470
1471                         self.number_selected(action, phoneNumber, message)
1472                         self._contactsviewselection.unselect_all()
1473                 except Exception, e:
1474                         self._errorDisplay.push_exception()