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