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