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