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