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