More touch selector fun (Callback number)
[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                 return make_ugly(self._callbackSelectButton.get_label())
748
749         def set_account_number(self, number):
750                 """
751                 Displays current account number
752                 """
753                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
754
755         def update(self, force = False):
756                 if not force and self._isPopulated:
757                         return False
758                 self._populate_callback_combo()
759                 self.set_account_number(self._backend.get_account_number())
760                 return True
761
762         def clear(self):
763                 self._callbackSelectButton.set_label("")
764                 self.set_account_number("")
765                 self._isPopulated = False
766
767         def save_everything(self):
768                 raise NotImplementedError
769
770         @staticmethod
771         def name():
772                 return "Account Info"
773
774         def load_settings(self, config, section):
775                 self._defaultCallback = config.get(section, "callback")
776                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
777                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
778                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
779
780         def save_settings(self, config, section):
781                 """
782                 @note Thread Agnostic
783                 """
784                 callback = self.get_selected_callback_number()
785                 config.set(section, "callback", callback)
786                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
787                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
788                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
789
790         def _populate_callback_combo(self):
791                 self._isPopulated = True
792                 del self._callbackList[:]
793                 try:
794                         callbackNumbers = self._backend.get_callback_numbers()
795                 except Exception, e:
796                         self._errorDisplay.push_exception()
797                         self._isPopulated = False
798                         return
799
800                 for number, description in callbackNumbers.iteritems():
801                         self._callbackList.append(make_pretty(number))
802
803                 callbackNumber = self._defaultCallback
804                 self._callbackSelectButton.set_label(make_pretty(callbackNumber))
805
806         def _set_callback_number(self, number):
807                 try:
808                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
809                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
810                         elif number == self._backend.get_callback_number():
811                                 logging.warning(
812                                         "Callback number already is %s" % (
813                                                 self._backend.get_callback_number(),
814                                         ),
815                                 )
816                         else:
817                                 self._backend.set_callback_number(number)
818                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
819                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
820                                 )
821                                 logging.info(
822                                         "Callback number set to %s" % (
823                                                 self._backend.get_callback_number(),
824                                         ),
825                                 )
826                 except Exception, e:
827                         self._errorDisplay.push_exception()
828
829         def _update_alarm_settings(self, recurrence):
830                 try:
831                         isEnabled = self._notifyCheckbox.get_active()
832                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
833                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
834                 finally:
835                         self.save_everything()
836                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
837                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
838
839         def _on_callbackentry_clicked(self, *args):
840                 try:
841                         actualSelection = make_pretty(self.get_selected_callback_number())
842
843                         userSelection = hildonize.touch_selector_entry(
844                                 self._window,
845                                 "Callback Number",
846                                 self._callbackList,
847                                 actualSelection,
848                         )
849                         number = make_ugly(userSelection)
850                         self._set_callback_number(number)
851                 except RuntimeError, e:
852                         logging.exception("%s" % str(e))
853                 except Exception, e:
854                         self._errorDisplay.push_exception()
855
856         def _on_notify_toggled(self, *args):
857                 try:
858                         if self._applyAlarmTimeoutId is not None:
859                                 gobject.source_remove(self._applyAlarmTimeoutId)
860                                 self._applyAlarmTimeoutId = None
861                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
862                 except Exception, e:
863                         self._errorDisplay.push_exception()
864
865         def _on_minutes_clicked(self, *args):
866                 recurrenceChoices = [
867                         (1, "1 minute"),
868                         (2, "2 minutes"),
869                         (3, "3 minutes"),
870                         (5, "5 minutes"),
871                         (8, "8 minutes"),
872                         (10, "10 minutes"),
873                         (15, "15 minutes"),
874                         (30, "30 minutes"),
875                         (45, "45 minutes"),
876                         (60, "1 hour"),
877                         (3*60, "3 hours"),
878                         (6*60, "6 hours"),
879                         (12*60, "12 hours"),
880                 ]
881                 try:
882                         actualSelection = self._alarmHandler.recurrence
883
884                         closestSelectionIndex = 0
885                         for i, possible in enumerate(recurrenceChoices):
886                                 if possible[0] <= actualSelection:
887                                         closestSelectionIndex = i
888                         recurrenceIndex = hildonize.touch_selector(
889                                 self._window,
890                                 "Minutes",
891                                 (("%s" % m[1]) for m in recurrenceChoices),
892                                 closestSelectionIndex,
893                         )
894                         recurrence = recurrenceChoices[recurrenceIndex][0]
895
896                         self._update_alarm_settings(recurrence)
897                 except RuntimeError, e:
898                         logging.exception("%s" % str(e))
899                 except Exception, e:
900                         self._errorDisplay.push_exception()
901
902         def _on_apply_timeout(self, *args):
903                 try:
904                         self._applyAlarmTimeoutId = None
905
906                         self._update_alarm_settings(self._alarmHandler.recurrence)
907                 except Exception, e:
908                         self._errorDisplay.push_exception()
909                 return False
910
911         def _on_missed_toggled(self, *args):
912                 try:
913                         self._notifyOnMissed = self._missedCheckbox.get_active()
914                         self.save_everything()
915                 except Exception, e:
916                         self._errorDisplay.push_exception()
917
918         def _on_voicemail_toggled(self, *args):
919                 try:
920                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
921                         self.save_everything()
922                 except Exception, e:
923                         self._errorDisplay.push_exception()
924
925         def _on_sms_toggled(self, *args):
926                 try:
927                         self._notifyOnSms = self._smsCheckbox.get_active()
928                         self.save_everything()
929                 except Exception, e:
930                         self._errorDisplay.push_exception()
931
932
933 class RecentCallsView(object):
934
935         NUMBER_IDX = 0
936         DATE_IDX = 1
937         ACTION_IDX = 2
938         FROM_IDX = 3
939
940         def __init__(self, widgetTree, backend, errorDisplay):
941                 self._errorDisplay = errorDisplay
942                 self._backend = backend
943
944                 self._isPopulated = False
945                 self._recentmodel = gtk.ListStore(
946                         gobject.TYPE_STRING, # number
947                         gobject.TYPE_STRING, # date
948                         gobject.TYPE_STRING, # action
949                         gobject.TYPE_STRING, # from
950                 )
951                 self._recentview = widgetTree.get_widget("recentview")
952                 self._recentviewselection = None
953                 self._onRecentviewRowActivatedId = 0
954
955                 textrenderer = gtk.CellRendererText()
956                 textrenderer.set_property("yalign", 0)
957                 self._dateColumn = gtk.TreeViewColumn("Date")
958                 self._dateColumn.pack_start(textrenderer, expand=True)
959                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
960
961                 textrenderer = gtk.CellRendererText()
962                 textrenderer.set_property("yalign", 0)
963                 self._actionColumn = gtk.TreeViewColumn("Action")
964                 self._actionColumn.pack_start(textrenderer, expand=True)
965                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
966
967                 textrenderer = gtk.CellRendererText()
968                 textrenderer.set_property("yalign", 0)
969                 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
970                 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
971                 self._numberColumn = gtk.TreeViewColumn("Number")
972                 self._numberColumn.pack_start(textrenderer, expand=True)
973                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
974
975                 textrenderer = gtk.CellRendererText()
976                 textrenderer.set_property("yalign", 0)
977                 hildonize.set_cell_thumb_selectable(textrenderer)
978                 self._nameColumn = gtk.TreeViewColumn("From")
979                 self._nameColumn.pack_start(textrenderer, expand=True)
980                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
981                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
982
983                 self._window = gtk_toolbox.find_parent_window(self._recentview)
984                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
985
986                 self._updateSink = gtk_toolbox.threaded_stage(
987                         gtk_toolbox.comap(
988                                 self._idly_populate_recentview,
989                                 gtk_toolbox.null_sink(),
990                         )
991                 )
992
993         def enable(self):
994                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
995                 self._recentview.set_model(self._recentmodel)
996
997                 self._recentview.append_column(self._dateColumn)
998                 self._recentview.append_column(self._actionColumn)
999                 self._recentview.append_column(self._numberColumn)
1000                 self._recentview.append_column(self._nameColumn)
1001                 self._recentviewselection = self._recentview.get_selection()
1002                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
1003
1004                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
1005
1006         def disable(self):
1007                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
1008
1009                 self.clear()
1010
1011                 self._recentview.remove_column(self._dateColumn)
1012                 self._recentview.remove_column(self._actionColumn)
1013                 self._recentview.remove_column(self._nameColumn)
1014                 self._recentview.remove_column(self._numberColumn)
1015                 self._recentview.set_model(None)
1016
1017         def number_selected(self, action, number, message):
1018                 """
1019                 @note Actual dial function is patched in later
1020                 """
1021                 raise NotImplementedError("Horrible unknown error has occurred")
1022
1023         def update(self, force = False):
1024                 if not force and self._isPopulated:
1025                         return False
1026                 self._updateSink.send(())
1027                 return True
1028
1029         def clear(self):
1030                 self._isPopulated = False
1031                 self._recentmodel.clear()
1032
1033         @staticmethod
1034         def name():
1035                 return "Recent Calls"
1036
1037         def load_settings(self, config, section):
1038                 pass
1039
1040         def save_settings(self, config, section):
1041                 """
1042                 @note Thread Agnostic
1043                 """
1044                 pass
1045
1046         def _idly_populate_recentview(self):
1047                 with gtk_toolbox.gtk_lock():
1048                         banner = hildonize.show_busy_banner_start(self._window, "Loading Recent History")
1049                 try:
1050                         self._recentmodel.clear()
1051                         self._isPopulated = True
1052
1053                         try:
1054                                 recentItems = self._backend.get_recent()
1055                         except Exception, e:
1056                                 self._errorDisplay.push_exception_with_lock()
1057                                 self._isPopulated = False
1058                                 recentItems = []
1059
1060                         for personName, phoneNumber, date, action in recentItems:
1061                                 if not personName:
1062                                         personName = "Unknown"
1063                                 date = abbrev_relative_date(date)
1064                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1065                                 prettyNumber = make_pretty(prettyNumber)
1066                                 item = (prettyNumber, date, action.capitalize(), personName)
1067                                 with gtk_toolbox.gtk_lock():
1068                                         self._recentmodel.append(item)
1069                 except Exception, e:
1070                         self._errorDisplay.push_exception_with_lock()
1071                 finally:
1072                         with gtk_toolbox.gtk_lock():
1073                                 hildonize.show_busy_banner_end(banner)
1074
1075                 return False
1076
1077         def _on_recentview_row_activated(self, treeview, path, view_column):
1078                 try:
1079                         model, itr = self._recentviewselection.get_selected()
1080                         if not itr:
1081                                 return
1082
1083                         number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
1084                         number = make_ugly(number)
1085                         contactPhoneNumbers = [("Phone", number)]
1086                         description = self._recentmodel.get_value(itr, self.FROM_IDX)
1087
1088                         action, phoneNumber, message = self._phoneTypeSelector.run(
1089                                 contactPhoneNumbers,
1090                                 messages = (description, ),
1091                                 parent = self._window,
1092                         )
1093                         if action == PhoneTypeSelector.ACTION_CANCEL:
1094                                 return
1095                         assert phoneNumber, "A lack of phone number exists"
1096
1097                         self.number_selected(action, phoneNumber, message)
1098                         self._recentviewselection.unselect_all()
1099                 except Exception, e:
1100                         self._errorDisplay.push_exception()
1101
1102
1103 class MessagesView(object):
1104
1105         NUMBER_IDX = 0
1106         DATE_IDX = 1
1107         HEADER_IDX = 2
1108         MESSAGE_IDX = 3
1109         MESSAGES_IDX = 4
1110
1111         def __init__(self, widgetTree, backend, errorDisplay):
1112                 self._errorDisplay = errorDisplay
1113                 self._backend = backend
1114
1115                 self._isPopulated = False
1116                 self._messagemodel = gtk.ListStore(
1117                         gobject.TYPE_STRING, # number
1118                         gobject.TYPE_STRING, # date
1119                         gobject.TYPE_STRING, # header
1120                         gobject.TYPE_STRING, # message
1121                         object, # messages
1122                 )
1123                 self._messageview = widgetTree.get_widget("messages_view")
1124                 self._messageviewselection = None
1125                 self._onMessageviewRowActivatedId = 0
1126
1127                 self._messageRenderer = gtk.CellRendererText()
1128                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1129                 self._messageRenderer.set_property("wrap-width", 500)
1130                 self._messageColumn = gtk.TreeViewColumn("Messages")
1131                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1132                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1133                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1134
1135                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1136                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1137
1138                 self._updateSink = gtk_toolbox.threaded_stage(
1139                         gtk_toolbox.comap(
1140                                 self._idly_populate_messageview,
1141                                 gtk_toolbox.null_sink(),
1142                         )
1143                 )
1144
1145         def enable(self):
1146                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1147                 self._messageview.set_model(self._messagemodel)
1148
1149                 self._messageview.append_column(self._messageColumn)
1150                 self._messageviewselection = self._messageview.get_selection()
1151                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1152
1153                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1154
1155         def disable(self):
1156                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1157
1158                 self.clear()
1159
1160                 self._messageview.remove_column(self._messageColumn)
1161                 self._messageview.set_model(None)
1162
1163         def number_selected(self, action, number, message):
1164                 """
1165                 @note Actual dial function is patched in later
1166                 """
1167                 raise NotImplementedError("Horrible unknown error has occurred")
1168
1169         def update(self, force = False):
1170                 if not force and self._isPopulated:
1171                         return False
1172                 self._updateSink.send(())
1173                 return True
1174
1175         def clear(self):
1176                 self._isPopulated = False
1177                 self._messagemodel.clear()
1178
1179         @staticmethod
1180         def name():
1181                 return "Messages"
1182
1183         def load_settings(self, config, section):
1184                 pass
1185
1186         def save_settings(self, config, section):
1187                 """
1188                 @note Thread Agnostic
1189                 """
1190                 pass
1191
1192         def _idly_populate_messageview(self):
1193                 with gtk_toolbox.gtk_lock():
1194                         banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1195                 try:
1196                         self._messagemodel.clear()
1197                         self._isPopulated = True
1198
1199                         try:
1200                                 messageItems = self._backend.get_messages()
1201                         except Exception, e:
1202                                 self._errorDisplay.push_exception_with_lock()
1203                                 self._isPopulated = False
1204                                 messageItems = []
1205
1206                         for header, number, relativeDate, messages in messageItems:
1207                                 prettyNumber = number[2:] if number.startswith("+1") else number
1208                                 prettyNumber = make_pretty(prettyNumber)
1209
1210                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1211                                 newMessages = [firstMessage]
1212                                 newMessages.extend(messages)
1213
1214                                 number = make_ugly(number)
1215
1216                                 row = (number, relativeDate, header, "\n".join(newMessages), newMessages)
1217                                 with gtk_toolbox.gtk_lock():
1218                                         self._messagemodel.append(row)
1219                 except Exception, e:
1220                         self._errorDisplay.push_exception_with_lock()
1221                 finally:
1222                         with gtk_toolbox.gtk_lock():
1223                                 hildonize.show_busy_banner_end(banner)
1224
1225                 return False
1226
1227         def _on_messageview_row_activated(self, treeview, path, view_column):
1228                 try:
1229                         model, itr = self._messageviewselection.get_selected()
1230                         if not itr:
1231                                 return
1232
1233                         contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1234                         description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1235
1236                         action, phoneNumber, message = self._phoneTypeSelector.run(
1237                                 contactPhoneNumbers,
1238                                 messages = description,
1239                                 parent = self._window,
1240                         )
1241                         if action == PhoneTypeSelector.ACTION_CANCEL:
1242                                 return
1243                         assert phoneNumber, "A lock of phone number exists"
1244
1245                         self.number_selected(action, phoneNumber, message)
1246                         self._messageviewselection.unselect_all()
1247                 except Exception, e:
1248                         self._errorDisplay.push_exception()
1249
1250
1251 class ContactsView(object):
1252
1253         def __init__(self, widgetTree, backend, errorDisplay):
1254                 self._errorDisplay = errorDisplay
1255                 self._backend = backend
1256
1257                 self._addressBook = None
1258                 self._selectedComboIndex = 0
1259                 self._addressBookFactories = [null_backend.NullAddressBook()]
1260
1261                 self._booksList = []
1262                 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1263
1264                 self._isPopulated = False
1265                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1266                 self._contactsviewselection = None
1267                 self._contactsview = widgetTree.get_widget("contactsview")
1268
1269                 self._contactColumn = gtk.TreeViewColumn("Contact")
1270                 displayContactSource = False
1271                 if displayContactSource:
1272                         textrenderer = gtk.CellRendererText()
1273                         self._contactColumn.pack_start(textrenderer, expand=False)
1274                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1275                 textrenderer = gtk.CellRendererText()
1276                 hildonize.set_cell_thumb_selectable(textrenderer)
1277                 self._contactColumn.pack_start(textrenderer, expand=True)
1278                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1279                 textrenderer = gtk.CellRendererText()
1280                 self._contactColumn.pack_start(textrenderer, expand=True)
1281                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1282                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1283                 self._contactColumn.set_sort_column_id(1)
1284                 self._contactColumn.set_visible(True)
1285
1286                 self._onContactsviewRowActivatedId = 0
1287                 self._onAddressbookButtonChangedId = 0
1288                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1289                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1290
1291                 self._updateSink = gtk_toolbox.threaded_stage(
1292                         gtk_toolbox.comap(
1293                                 self._idly_populate_contactsview,
1294                                 gtk_toolbox.null_sink(),
1295                         )
1296                 )
1297
1298         def enable(self):
1299                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1300
1301                 self._contactsview.set_model(self._contactsmodel)
1302                 self._contactsview.append_column(self._contactColumn)
1303                 self._contactsviewselection = self._contactsview.get_selection()
1304                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1305
1306                 del self._booksList[:]
1307                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1308                         if factoryName and bookName:
1309                                 entryName = "%s: %s" % (factoryName, bookName)
1310                         elif factoryName:
1311                                 entryName = factoryName
1312                         elif bookName:
1313                                 entryName = bookName
1314                         else:
1315                                 entryName = "Bad name (%d)" % factoryId
1316                         row = (str(factoryId), bookId, entryName)
1317                         self._booksList.append(row)
1318
1319                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1320                 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1321
1322                 if len(self._booksList) <= self._selectedComboIndex:
1323                         self._selectedComboIndex = 0
1324                 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1325
1326                 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1327                 selectedBookId = self._booksList[self._selectedComboIndex][1]
1328                 self.open_addressbook(selectedFactoryId, selectedBookId)
1329
1330         def disable(self):
1331                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1332                 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1333
1334                 self.clear()
1335
1336                 self._bookSelectionButton.set_label("")
1337                 self._contactsview.set_model(None)
1338                 self._contactsview.remove_column(self._contactColumn)
1339
1340         def number_selected(self, action, number, message):
1341                 """
1342                 @note Actual dial function is patched in later
1343                 """
1344                 raise NotImplementedError("Horrible unknown error has occurred")
1345
1346         def get_addressbooks(self):
1347                 """
1348                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1349                 """
1350                 for i, factory in enumerate(self._addressBookFactories):
1351                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1352                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1353
1354         def open_addressbook(self, bookFactoryId, bookId):
1355                 bookFactoryIndex = int(bookFactoryId)
1356                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1357
1358                 forceUpdate = True if addressBook is not self._addressBook else False
1359
1360                 self._addressBook = addressBook
1361                 self.update(force=forceUpdate)
1362
1363         def update(self, force = False):
1364                 if not force and self._isPopulated:
1365                         return False
1366                 self._updateSink.send(())
1367                 return True
1368
1369         def clear(self):
1370                 self._isPopulated = False
1371                 self._contactsmodel.clear()
1372                 for factory in self._addressBookFactories:
1373                         factory.clear_caches()
1374                 self._addressBook.clear_caches()
1375
1376         def append(self, book):
1377                 self._addressBookFactories.append(book)
1378
1379         def extend(self, books):
1380                 self._addressBookFactories.extend(books)
1381
1382         @staticmethod
1383         def name():
1384                 return "Contacts"
1385
1386         def load_settings(self, config, sectionName):
1387                 try:
1388                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1389                 except ConfigParser.NoOptionError:
1390                         self._selectedComboIndex = 0
1391
1392         def save_settings(self, config, sectionName):
1393                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1394
1395         def _idly_populate_contactsview(self):
1396                 with gtk_toolbox.gtk_lock():
1397                         banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1398                 try:
1399                         addressBook = None
1400                         while addressBook is not self._addressBook:
1401                                 addressBook = self._addressBook
1402                                 with gtk_toolbox.gtk_lock():
1403                                         self._contactsview.set_model(None)
1404                                         self.clear()
1405
1406                                 try:
1407                                         contacts = addressBook.get_contacts()
1408                                 except Exception, e:
1409                                         contacts = []
1410                                         self._isPopulated = False
1411                                         self._errorDisplay.push_exception_with_lock()
1412                                 for contactId, contactName in contacts:
1413                                         contactType = (addressBook.contact_source_short_name(contactId), )
1414                                         self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1415
1416                                 with gtk_toolbox.gtk_lock():
1417                                         self._contactsview.set_model(self._contactsmodel)
1418
1419                         self._isPopulated = True
1420                 except Exception, e:
1421                         self._errorDisplay.push_exception_with_lock()
1422                 finally:
1423                         with gtk_toolbox.gtk_lock():
1424                                 hildonize.show_busy_banner_end(banner)
1425                 return False
1426
1427         def _on_addressbook_button_changed(self, *args, **kwds):
1428                 try:
1429                         try:
1430                                 newSelectedComboIndex = hildonize.touch_selector(
1431                                         self._window,
1432                                         "Addressbook",
1433                                         (("%s" % m[2]) for m in self._booksList),
1434                                         self._selectedComboIndex,
1435                                 )
1436                         except RuntimeError:
1437                                 return
1438
1439                         selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1440                         selectedBookId = self._booksList[newSelectedComboIndex][1]
1441                         self.open_addressbook(selectedFactoryId, selectedBookId)
1442                         self._selectedComboIndex = newSelectedComboIndex
1443                         self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1444                 except Exception, e:
1445                         self._errorDisplay.push_exception()
1446
1447         def _on_contactsview_row_activated(self, treeview, path, view_column):
1448                 try:
1449                         model, itr = self._contactsviewselection.get_selected()
1450                         if not itr:
1451                                 return
1452
1453                         contactId = self._contactsmodel.get_value(itr, 3)
1454                         contactName = self._contactsmodel.get_value(itr, 1)
1455                         try:
1456                                 contactDetails = self._addressBook.get_contact_details(contactId)
1457                         except Exception, e:
1458                                 contactDetails = []
1459                                 self._errorDisplay.push_exception()
1460                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1461
1462                         if len(contactPhoneNumbers) == 0:
1463                                 return
1464
1465                         action, phoneNumber, message = self._phoneTypeSelector.run(
1466                                 contactPhoneNumbers,
1467                                 messages = (contactName, ),
1468                                 parent = self._window,
1469                         )
1470                         if action == PhoneTypeSelector.ACTION_CANCEL:
1471                                 return
1472                         assert phoneNumber, "A lack of phone number exists"
1473
1474                         self.number_selected(action, phoneNumber, message)
1475                         self._contactsviewselection.unselect_all()
1476                 except Exception, e:
1477                         self._errorDisplay.push_exception()