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