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