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