Bumpting Qt version
[gc-dialer] / src / gv_views.py
1 #!/usr/bin/env python
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 """
23
24 from __future__ import with_statement
25
26 import re
27 import ConfigParser
28 import itertools
29 import logging
30
31 import gobject
32 import pango
33 import gtk
34
35 import gtk_toolbox
36 import hildonize
37 from backends import gv_backend
38 from backends 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         return normalize_number(prettynumber)
53
54
55 def normalize_number(prettynumber):
56         """
57         function to take a phone number and strip out all non-numeric
58         characters
59
60         >>> normalize_number("+012-(345)-678-90")
61         '+01234567890'
62         >>> normalize_number("1-(345)-678-9000")
63         '+13456789000'
64         >>> normalize_number("+1-(345)-678-9000")
65         '+13456789000'
66         """
67         uglynumber = re.sub('[^0-9+]', '', prettynumber)
68
69         if uglynumber.startswith("+"):
70                 pass
71         elif uglynumber.startswith("1"):
72                 uglynumber = "+"+uglynumber
73         elif 10 <= len(uglynumber):
74                 assert uglynumber[0] not in ("+", "1")
75                 uglynumber = "+1"+uglynumber
76         else:
77                 pass
78
79         return uglynumber
80
81
82 def _make_pretty_with_areacode(phonenumber):
83         prettynumber = "(%s)" % (phonenumber[0:3], )
84         if 3 < len(phonenumber):
85                 prettynumber += " %s" % (phonenumber[3:6], )
86                 if 6 < len(phonenumber):
87                         prettynumber += "-%s" % (phonenumber[6:], )
88         return prettynumber
89
90
91 def _make_pretty_local(phonenumber):
92         prettynumber = "%s" % (phonenumber[0:3], )
93         if 3 < len(phonenumber):
94                 prettynumber += "-%s" % (phonenumber[3:], )
95         return prettynumber
96
97
98 def _make_pretty_international(phonenumber):
99         prettynumber = phonenumber
100         if phonenumber.startswith("1"):
101                 prettynumber = "1 "
102                 prettynumber += _make_pretty_with_areacode(phonenumber[1:])
103         return prettynumber
104
105
106 def make_pretty(phonenumber):
107         """
108         Function to take a phone number and return the pretty version
109         pretty numbers:
110                 if phonenumber begins with 0:
111                         ...-(...)-...-....
112                 if phonenumber begins with 1: ( for gizmo callback numbers )
113                         1 (...)-...-....
114                 if phonenumber is 13 digits:
115                         (...)-...-....
116                 if phonenumber is 10 digits:
117                         ...-....
118         >>> make_pretty("12")
119         '12'
120         >>> make_pretty("1234567")
121         '123-4567'
122         >>> make_pretty("2345678901")
123         '+1 (234) 567-8901'
124         >>> make_pretty("12345678901")
125         '+1 (234) 567-8901'
126         >>> make_pretty("01234567890")
127         '+012 (345) 678-90'
128         >>> make_pretty("+01234567890")
129         '+012 (345) 678-90'
130         >>> make_pretty("+12")
131         '+1 (2)'
132         >>> make_pretty("+123")
133         '+1 (23)'
134         >>> make_pretty("+1234")
135         '+1 (234)'
136         """
137         if phonenumber is None or phonenumber == "":
138                 return ""
139
140         phonenumber = normalize_number(phonenumber)
141
142         if phonenumber == "":
143                 return ""
144         elif phonenumber[0] == "+":
145                 prettynumber = _make_pretty_international(phonenumber[1:])
146                 if not prettynumber.startswith("+"):
147                         prettynumber = "+"+prettynumber
148         elif 8 < len(phonenumber) and phonenumber[0] in ("1", ):
149                 prettynumber = _make_pretty_international(phonenumber)
150         elif 7 < len(phonenumber):
151                 prettynumber = _make_pretty_with_areacode(phonenumber)
152         elif 3 < len(phonenumber):
153                 prettynumber = _make_pretty_local(phonenumber)
154         else:
155                 prettynumber = phonenumber
156         return prettynumber.strip()
157
158
159 def abbrev_relative_date(date):
160         """
161         >>> abbrev_relative_date("42 hours ago")
162         '42 h'
163         >>> abbrev_relative_date("2 days ago")
164         '2 d'
165         >>> abbrev_relative_date("4 weeks ago")
166         '4 w'
167         """
168         parts = date.split(" ")
169         return "%s %s" % (parts[0], parts[1][0])
170
171
172 def _collapse_message(messageLines, maxCharsPerLine, maxLines):
173         lines = 0
174
175         numLines = len(messageLines)
176         for line in messageLines[0:min(maxLines, numLines)]:
177                 linesPerLine = max(1, int(len(line) / maxCharsPerLine))
178                 allowedLines = maxLines - lines
179                 acceptedLines = min(allowedLines, linesPerLine)
180                 acceptedChars = acceptedLines * maxCharsPerLine
181
182                 if acceptedChars < (len(line) + 3):
183                         suffix = "..."
184                 else:
185                         acceptedChars = len(line) # eh, might as well complete the line
186                         suffix = ""
187                 abbrevMessage = "%s%s" % (line[0:acceptedChars], suffix)
188                 yield abbrevMessage
189
190                 lines += acceptedLines
191                 if maxLines <= lines:
192                         break
193
194
195 def collapse_message(message, maxCharsPerLine, maxLines):
196         r"""
197         >>> collapse_message("Hello", 60, 2)
198         'Hello'
199         >>> collapse_message("Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789", 60, 2)
200         'Hello world how are you doing today? 01234567890123456789012...'
201         >>> collapse_message('''Hello world how are you doing today?
202         ... 01234567890123456789
203         ... 01234567890123456789
204         ... 01234567890123456789
205         ... 01234567890123456789''', 60, 2)
206         'Hello world how are you doing today?\n01234567890123456789'
207         >>> collapse_message('''
208         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
209         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
210         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
211         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
212         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789
213         ... Hello world how are you doing today? 01234567890123456789012345678901234567890123456789012345678901234567890123456789''', 60, 2)
214         '\nHello world how are you doing today? 01234567890123456789012...'
215         """
216         messageLines = message.split("\n")
217         return "\n".join(_collapse_message(messageLines, maxCharsPerLine, maxLines))
218
219
220 def _get_contact_numbers(backend, contactId, number):
221         contactPhoneNumbers = []
222         if contactId and contactId != '0':
223                 try:
224                         contactPhoneNumbers = list(backend.get_contact_details(contactId))
225                 except KeyError:
226                         contactPhoneNumbers = []
227                 if contactPhoneNumbers:
228                         uglyContactNumbers = (
229                                 make_ugly(contactNumber)
230                                 for (numberDescription, contactNumber) in contactPhoneNumbers
231                         )
232                         defaultMatches = [
233                                 (
234                                         number == contactNumber or
235                                         number[1:] == contactNumber and number.startswith("1") or
236                                         number[2:] == contactNumber and number.startswith("+1") or
237                                         number == contactNumber[1:] and contactNumber.startswith("1") or
238                                         number == contactNumber[2:] and contactNumber.startswith("+1")
239                                 )
240                                 for contactNumber in uglyContactNumbers
241                         ]
242                         try:
243                                 defaultIndex = defaultMatches.index(True)
244                         except ValueError:
245                                 contactPhoneNumbers.append(("Other", number))
246                                 defaultIndex = len(contactPhoneNumbers)-1
247                                 _moduleLogger.warn(
248                                         "Could not find contact %r's number %s among %r" % (
249                                                 contactId, number, contactPhoneNumbers
250                                         )
251                                 )
252
253         if not contactPhoneNumbers:
254                 contactPhoneNumbers = [("Phone", number)]
255                 defaultIndex = -1
256
257         return contactPhoneNumbers, defaultIndex
258
259
260 class SmsEntryWindow(object):
261
262         MAX_CHAR = 160
263
264         def __init__(self, widgetTree, parent, app):
265                 self._clipboard = gtk.clipboard_get()
266                 self._widgetTree = widgetTree
267                 self._parent = parent
268                 self._app = app
269                 self._isFullScreen = False
270
271                 self._window = self._widgetTree.get_widget("smsWindow")
272                 self._window = hildonize.hildonize_window(self._app, self._window)
273                 self._window.set_title("SMS")
274                 self._window.connect("delete-event", self._on_delete)
275                 self._window.connect("key-press-event", self._on_key_press)
276                 self._window.connect("window-state-event", self._on_window_state_change)
277                 self._widgetTree.get_widget("smsMessagesViewPort").get_parent().show()
278
279                 errorBox = self._widgetTree.get_widget("smsErrorEventBox")
280                 errorDescription = self._widgetTree.get_widget("smsErrorDescription")
281                 errorClose = self._widgetTree.get_widget("smsErrorClose")
282                 self._errorDisplay = gtk_toolbox.ErrorDisplay(errorBox, errorDescription, errorClose)
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
289                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
290
291                 self._messagemodel = gtk.ListStore(gobject.TYPE_STRING)
292                 self._messagesView = self._widgetTree.get_widget("smsMessages")
293
294                 textrenderer = gtk.CellRendererText()
295                 textrenderer.set_property("wrap-mode", pango.WRAP_WORD)
296                 textrenderer.set_property("wrap-width", 450)
297                 messageColumn = gtk.TreeViewColumn("")
298                 messageColumn.pack_start(textrenderer, expand=True)
299                 messageColumn.add_attribute(textrenderer, "markup", 0)
300                 messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
301                 self._messagesView.append_column(messageColumn)
302                 self._messagesView.set_headers_visible(False)
303                 self._messagesView.set_model(self._messagemodel)
304                 self._messagesView.set_fixed_height_mode(False)
305
306                 self._conversationView = self._messagesView.get_parent()
307                 self._conversationViewPort = self._conversationView.get_parent()
308                 self._scrollWindow = self._conversationViewPort.get_parent()
309
310                 self._targetList = self._widgetTree.get_widget("smsTargetList")
311                 self._phoneButton = self._widgetTree.get_widget("phoneTypeSelection")
312                 self._phoneButton.connect("clicked", self._on_phone)
313                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
314                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
315                 self._smsEntrySize = None
316
317                 self._contacts = []
318
319         def add_contact(self, name, contactDetails, messages = (), defaultIndex = -1):
320                 contactNumbers = list(self._to_contact_numbers(contactDetails))
321                 assert contactNumbers, "Contact must have at least one number"
322                 contactIndex = defaultIndex if defaultIndex != -1 else 0
323                 contact = contactNumbers, contactIndex, messages
324                 self._contacts.append(contact)
325
326                 nameLabel = gtk.Label(name)
327                 selector = gtk.Button(contactNumbers[0][1])
328                 if len(contactNumbers) == 1:
329                         selector.set_sensitive(False)
330                 removeContact = gtk.Button(stock="gtk-delete")
331                 row = gtk.HBox()
332                 row.pack_start(nameLabel, True, True)
333                 row.pack_start(selector, True, True)
334                 row.pack_start(removeContact, False, False)
335                 row.show_all()
336                 self._targetList.pack_start(row)
337                 selector.connect("clicked", self._on_choose_phone_n, row)
338                 removeContact.connect("clicked", self._on_remove_phone_n, row)
339                 self._update_button_state()
340                 self._update_context()
341
342                 parentSize = self._parent.get_size()
343                 self._window.resize(parentSize[0], max(parentSize[1]-10, 100))
344                 self._window.show()
345                 self._window.present()
346
347                 self._smsEntry.grab_focus()
348                 self._scroll_to_bottom()
349
350         def clear(self):
351                 del self._contacts[:]
352
353                 for row in list(self._targetList.get_children()):
354                         self._targetList.remove(row)
355                 self._smsEntry.get_buffer().set_text("")
356                 self._update_letter_count()
357                 self._update_context()
358
359         def fullscreen(self):
360                 self._window.fullscreen()
361
362         def unfullscreen(self):
363                 self._window.unfullscreen()
364
365         def _remove_contact(self, contactIndex):
366                 del self._contacts[contactIndex]
367
368                 row = list(self._targetList.get_children())[contactIndex]
369                 self._targetList.remove(row)
370                 self._update_button_state()
371                 self._update_context()
372                 self._scroll_to_bottom()
373
374         def _scroll_to_bottom(self):
375                 dx = self._conversationView.get_allocation().height - self._conversationViewPort.get_allocation().height
376                 dx = max(dx, 0)
377                 adjustment = self._scrollWindow.get_vadjustment()
378                 adjustment.value = dx
379
380         def _update_letter_count(self):
381                 if self._smsEntrySize is None:
382                         self._smsEntrySize = self._smsEntry.size_request()
383                 else:
384                         self._smsEntry.set_size_request(*self._smsEntrySize)
385                 entryLength = self._smsEntry.get_buffer().get_char_count()
386
387                 numTexts, numCharInText = divmod(entryLength, self.MAX_CHAR)
388                 if numTexts:
389                         self._letterCountLabel.set_text("%s.%s" % (numTexts, numCharInText))
390                 else:
391                         self._letterCountLabel.set_text("%s" % (numCharInText, ))
392
393                 self._update_button_state()
394
395         def _update_context(self):
396                 self._messagemodel.clear()
397                 if len(self._contacts) == 0:
398                         self._messagesView.hide()
399                         self._targetList.hide()
400                         self._phoneButton.hide()
401                         self._phoneButton.set_label("Error: You shouldn't see this")
402                 elif len(self._contacts) == 1:
403                         contactNumbers, index, messages = self._contacts[0]
404                         if messages:
405                                 self._messagesView.show()
406                                 for message in messages:
407                                         row = (message, )
408                                         self._messagemodel.append(row)
409                                 messagesSelection = self._messagesView.get_selection()
410                                 messagesSelection.select_path((len(messages)-1, ))
411                         else:
412                                 self._messagesView.hide()
413                         self._targetList.hide()
414                         self._phoneButton.show()
415                         self._phoneButton.set_label(contactNumbers[index][1])
416                         if 1 < len(contactNumbers):
417                                 self._phoneButton.set_sensitive(True)
418                         else:
419                                 self._phoneButton.set_sensitive(False)
420                 else:
421                         self._messagesView.hide()
422                         self._targetList.show()
423                         self._phoneButton.hide()
424                         self._phoneButton.set_label("Error: You shouldn't see this")
425
426         def _update_button_state(self):
427                 if len(self._contacts) == 0:
428                         self._dialButton.set_sensitive(False)
429                         self._smsButton.set_sensitive(False)
430                 elif len(self._contacts) == 1:
431                         entryLength = self._smsEntry.get_buffer().get_char_count()
432                         if entryLength == 0:
433                                 self._dialButton.set_sensitive(True)
434                                 self._smsButton.set_sensitive(False)
435                         else:
436                                 self._dialButton.set_sensitive(False)
437                                 self._smsButton.set_sensitive(True)
438                 else:
439                         self._dialButton.set_sensitive(False)
440                         self._smsButton.set_sensitive(True)
441
442         def _to_contact_numbers(self, contactDetails):
443                 for phoneType, phoneNumber in contactDetails:
444                         display = " - ".join((make_pretty(phoneNumber), phoneType))
445                         yield (phoneNumber, display)
446
447         def _pseudo_destroy(self):
448                 self.clear()
449                 self._window.hide()
450
451         def _request_number(self, contactIndex):
452                 contactNumbers, index, messages = self._contacts[contactIndex]
453                 assert 0 <= index, "%r" % index
454
455                 index = hildonize.touch_selector(
456                         self._window,
457                         "Phone Numbers",
458                         (description for (number, description) in contactNumbers),
459                         index,
460                 )
461                 self._contacts[contactIndex] = contactNumbers, index, messages
462
463         def send_sms(self, numbers, message):
464                 raise NotImplementedError()
465
466         def dial(self, number):
467                 raise NotImplementedError()
468
469         def _on_phone(self, *args):
470                 try:
471                         assert len(self._contacts) == 1, "One and only one contact is required"
472                         self._request_number(0)
473
474                         contactNumbers, numberIndex, messages = self._contacts[0]
475                         self._phoneButton.set_label(contactNumbers[numberIndex][1])
476                         row = list(self._targetList.get_children())[0]
477                         phoneButton = list(row.get_children())[1]
478                         phoneButton.set_label(contactNumbers[numberIndex][1])
479                 except Exception, e:
480                         self._errorDisplay.push_exception()
481
482         def _on_choose_phone_n(self, button, row):
483                 try:
484                         assert 1 < len(self._contacts), "More than one contact required"
485                         targetList = list(self._targetList.get_children())
486                         index = targetList.index(row)
487                         self._request_number(index)
488
489                         contactNumbers, numberIndex, messages = self._contacts[0]
490                         phoneButton = list(row.get_children())[1]
491                         phoneButton.set_label(contactNumbers[numberIndex][1])
492                 except Exception, e:
493                         self._errorDisplay.push_exception()
494
495         def _on_remove_phone_n(self, button, row):
496                 try:
497                         assert 1 < len(self._contacts), "More than one contact required"
498                         targetList = list(self._targetList.get_children())
499                         index = targetList.index(row)
500
501                         del self._contacts[index]
502                         self._targetList.remove(row)
503                         self._update_context()
504                         self._update_button_state()
505                 except Exception, e:
506                         self._errorDisplay.push_exception()
507
508         def _on_entry_changed(self, *args):
509                 try:
510                         self._update_letter_count()
511                 except Exception, e:
512                         self._errorDisplay.push_exception()
513
514         def _on_send(self, *args):
515                 try:
516                         assert 0 < len(self._contacts), "At least one contact required (%r)" % self._contacts
517                         phoneNumbers = [
518                                 make_ugly(contact[0][contact[1]][0])
519                                 for contact in self._contacts
520                         ]
521
522                         entryBuffer = self._smsEntry.get_buffer()
523                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
524                         enteredMessage = enteredMessage.strip()
525                         assert enteredMessage, "No message provided"
526                         self.send_sms(phoneNumbers, enteredMessage)
527                         self._pseudo_destroy()
528                 except Exception, e:
529                         self._errorDisplay.push_exception()
530
531         def _on_dial(self, *args):
532                 try:
533                         assert len(self._contacts) == 1, "One and only one contact allowed (%r)" % self._contacts
534                         contact = self._contacts[0]
535                         contactNumber = contact[0][contact[1]][0]
536                         phoneNumber = make_ugly(contactNumber)
537                         self.dial(phoneNumber)
538                         self._pseudo_destroy()
539                 except Exception, e:
540                         self._errorDisplay.push_exception()
541
542         def _on_delete(self, *args):
543                 try:
544                         self._window.emit_stop_by_name("delete-event")
545                         if hildonize.IS_FREMANTLE_SUPPORTED:
546                                 self._window.hide()
547                         else:
548                                 self._pseudo_destroy()
549                 except Exception, e:
550                         self._errorDisplay.push_exception()
551                 return True
552
553         def _on_window_state_change(self, widget, event, *args):
554                 try:
555                         if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
556                                 self._isFullScreen = True
557                         else:
558                                 self._isFullScreen = False
559                 except Exception, e:
560                         self._errorDisplay.push_exception()
561
562         def _on_key_press(self, widget, event):
563                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
564                 try:
565                         if (
566                                 event.keyval == gtk.keysyms.F6 or
567                                 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
568                         ):
569                                 if self._isFullScreen:
570                                         self._window.unfullscreen()
571                                 else:
572                                         self._window.fullscreen()
573                         elif event.keyval == ord("c") and event.get_state() & gtk.gdk.CONTROL_MASK:
574                                 message = "\n".join(
575                                         messagePart[0]
576                                         for messagePart in self._messagemodel
577                                 )
578                                 self._clipboard.set_text(str(message))
579                         elif (
580                                 event.keyval == gtk.keysyms.h and
581                                 event.get_state() & gtk.gdk.CONTROL_MASK
582                         ):
583                                 self._window.hide()
584                         elif (
585                                 event.keyval == gtk.keysyms.w and
586                                 event.get_state() & gtk.gdk.CONTROL_MASK
587                         ):
588                                 self._pseudo_destroy()
589                         elif (
590                                 event.keyval == gtk.keysyms.q and
591                                 event.get_state() & gtk.gdk.CONTROL_MASK
592                         ):
593                                 self._parent.destroy()
594                 except Exception, e:
595                         self._errorDisplay.push_exception()
596
597
598 class Dialpad(object):
599
600         def __init__(self, widgetTree, errorDisplay):
601                 self._clipboard = gtk.clipboard_get()
602                 self._errorDisplay = errorDisplay
603
604                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
605                 self._callButton = widgetTree.get_widget("dialpadCall")
606                 self._sendSMSButton = widgetTree.get_widget("dialpadSMS")
607                 self._backButton = widgetTree.get_widget("back")
608                 self._plusButton = widgetTree.get_widget("plus")
609                 self._phonenumber = ""
610                 self._prettynumber = ""
611
612                 callbackMapping = {
613                         "on_digit_clicked": self._on_digit_clicked,
614                 }
615                 widgetTree.signal_autoconnect(callbackMapping)
616                 self._sendSMSButton.connect("clicked", self._on_sms_clicked)
617                 self._callButton.connect("clicked", self._on_call_clicked)
618                 self._plusButton.connect("clicked", self._on_plus)
619
620                 self._originalLabel = self._backButton.get_label()
621                 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
622                 self._backTapHandler.on_tap = self._on_backspace
623                 self._backTapHandler.on_hold = self._on_clearall
624                 self._backTapHandler.on_holding = self._set_clear_button
625                 self._backTapHandler.on_cancel = self._reset_back_button
626
627                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
628                 self._keyPressEventId = 0
629
630         def enable(self):
631                 self._sendSMSButton.grab_focus()
632                 self._backTapHandler.enable()
633                 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
634
635         def disable(self):
636                 self._window.disconnect(self._keyPressEventId)
637                 self._keyPressEventId = 0
638                 self._reset_back_button()
639                 self._backTapHandler.disable()
640
641         def add_contact(self, *args, **kwds):
642                 """
643                 @note Actual function is patched in later
644                 """
645                 raise NotImplementedError("Horrible unknown error has occurred")
646
647         def dial(self, number):
648                 """
649                 @note Actual function is patched in later
650                 """
651                 raise NotImplementedError("Horrible unknown error has occurred")
652
653         def get_number(self):
654                 return self._phonenumber
655
656         def set_number(self, number):
657                 """
658                 Set the number to dial
659                 """
660                 try:
661                         self._phonenumber = make_ugly(number)
662                         self._prettynumber = make_pretty(self._phonenumber)
663                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
664                         if self._phonenumber:
665                                 self._plusButton.set_sensitive(False)
666                         else:
667                                 self._plusButton.set_sensitive(True)
668                 except TypeError, e:
669                         self._errorDisplay.push_exception()
670
671         def clear(self):
672                 self.set_number("")
673
674         @staticmethod
675         def name():
676                 return "Dialpad"
677
678         def load_settings(self, config, section):
679                 pass
680
681         def save_settings(self, config, section):
682                 """
683                 @note Thread Agnostic
684                 """
685                 pass
686
687         def set_orientation(self, orientation):
688                 if orientation == gtk.ORIENTATION_VERTICAL:
689                         pass
690                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
691                         pass
692                 else:
693                         raise NotImplementedError(orientation)
694
695         def _on_key_press(self, widget, event):
696                 try:
697                         if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
698                                 contents = self._clipboard.wait_for_text()
699                                 if contents is not None:
700                                         self.set_number(contents)
701                 except Exception, e:
702                         self._errorDisplay.push_exception()
703
704         def _on_call_clicked(self, widget):
705                 try:
706                         phoneNumber = self.get_number()
707                         self.dial(phoneNumber)
708                         self.set_number("")
709                 except Exception, e:
710                         self._errorDisplay.push_exception()
711
712         def _on_sms_clicked(self, widget):
713                 try:
714                         phoneNumber = self.get_number()
715                         self.add_contact(
716                                 "(Dialpad)",
717                                 [("Dialer", phoneNumber)], ()
718                         )
719                         self.set_number("")
720                 except Exception, e:
721                         self._errorDisplay.push_exception()
722
723         def _on_digit_clicked(self, widget):
724                 try:
725                         self.set_number(self._phonenumber + widget.get_name()[-1])
726                 except Exception, e:
727                         self._errorDisplay.push_exception()
728
729         def _on_plus(self, *args):
730                 try:
731                         self.set_number(self._phonenumber + "+")
732                 except Exception, e:
733                         self._errorDisplay.push_exception()
734
735         def _on_backspace(self, taps):
736                 try:
737                         self.set_number(self._phonenumber[:-taps])
738                         self._reset_back_button()
739                 except Exception, e:
740                         self._errorDisplay.push_exception()
741
742         def _on_clearall(self, taps):
743                 try:
744                         self.clear()
745                         self._reset_back_button()
746                 except Exception, e:
747                         self._errorDisplay.push_exception()
748                 return False
749
750         def _set_clear_button(self):
751                 try:
752                         self._backButton.set_label("gtk-clear")
753                 except Exception, e:
754                         self._errorDisplay.push_exception()
755
756         def _reset_back_button(self):
757                 try:
758                         self._backButton.set_label(self._originalLabel)
759                 except Exception, e:
760                         self._errorDisplay.push_exception()
761
762
763 class AccountInfo(object):
764
765         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
766                 self._errorDisplay = errorDisplay
767                 self._backend = backend
768                 self._isPopulated = False
769                 self._alarmHandler = alarmHandler
770                 self._notifyOnMissed = False
771                 self._notifyOnVoicemail = False
772                 self._notifyOnSms = False
773
774                 self._callbackList = []
775                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
776                 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
777                 self._onCallbackSelectChangedId = 0
778
779                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
780                 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
781                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
782                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
783                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
784                 self._onNotifyToggled = 0
785                 self._onMinutesChanged = 0
786                 self._onMissedToggled = 0
787                 self._onVoicemailToggled = 0
788                 self._onSmsToggled = 0
789                 self._applyAlarmTimeoutId = None
790
791                 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
792                 self._callbackNumber = ""
793
794         def enable(self):
795                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
796
797                 self._accountViewNumberDisplay.set_use_markup(True)
798                 self.set_account_number("")
799
800                 del self._callbackList[:]
801                 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
802                 self._set_callback_label("")
803
804                 if self._alarmHandler is not None:
805                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
806                         self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
807                         self._missedCheckbox.set_active(self._notifyOnMissed)
808                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
809                         self._smsCheckbox.set_active(self._notifyOnSms)
810
811                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
812                         self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
813                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
814                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
815                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
816                 else:
817                         self._notifyCheckbox.set_sensitive(False)
818                         self._minutesEntryButton.set_sensitive(False)
819                         self._missedCheckbox.set_sensitive(False)
820                         self._voicemailCheckbox.set_sensitive(False)
821                         self._smsCheckbox.set_sensitive(False)
822
823                 self.update(force=True)
824
825         def disable(self):
826                 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
827                 self._onCallbackSelectChangedId = 0
828                 self._set_callback_label("")
829
830                 if self._alarmHandler is not None:
831                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
832                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
833                         self._missedCheckbox.disconnect(self._onNotifyToggled)
834                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
835                         self._smsCheckbox.disconnect(self._onNotifyToggled)
836                         self._onNotifyToggled = 0
837                         self._onMinutesChanged = 0
838                         self._onMissedToggled = 0
839                         self._onVoicemailToggled = 0
840                         self._onSmsToggled = 0
841                 else:
842                         self._notifyCheckbox.set_sensitive(True)
843                         self._minutesEntryButton.set_sensitive(True)
844                         self._missedCheckbox.set_sensitive(True)
845                         self._voicemailCheckbox.set_sensitive(True)
846                         self._smsCheckbox.set_sensitive(True)
847
848                 self.clear()
849                 del self._callbackList[:]
850
851         def set_account_number(self, number):
852                 """
853                 Displays current account number
854                 """
855                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
856
857         def update(self, force = False):
858                 if not force and self._isPopulated:
859                         return False
860                 self._populate_callback_combo()
861                 self.set_account_number(self._backend.get_account_number())
862                 return True
863
864         def clear(self):
865                 self._set_callback_label("")
866                 self.set_account_number("")
867                 self._isPopulated = False
868
869         def save_everything(self):
870                 raise NotImplementedError
871
872         @staticmethod
873         def name():
874                 return "Account Info"
875
876         def load_settings(self, config, section):
877                 self._callbackNumber = make_ugly(config.get(section, "callback"))
878                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
879                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
880                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
881
882         def save_settings(self, config, section):
883                 """
884                 @note Thread Agnostic
885                 """
886                 config.set(section, "callback", self._callbackNumber)
887                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
888                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
889                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
890
891         def set_orientation(self, orientation):
892                 if orientation == gtk.ORIENTATION_VERTICAL:
893                         pass
894                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
895                         pass
896                 else:
897                         raise NotImplementedError(orientation)
898
899         def _populate_callback_combo(self):
900                 self._isPopulated = True
901                 del self._callbackList[:]
902                 try:
903                         callbackNumbers = self._backend.get_callback_numbers()
904                 except Exception, e:
905                         self._errorDisplay.push_exception()
906                         self._isPopulated = False
907                         return
908
909                 if len(callbackNumbers) == 0:
910                         callbackNumbers = {"": "No callback numbers available"}
911
912                 for number, description in callbackNumbers.iteritems():
913                         numberDisplay = make_pretty(number)
914                         if not numberDisplay:
915                                 continue
916                         self._callbackList.append((numberDisplay, description))
917
918                 self._set_callback_number(self._callbackNumber)
919
920         def _set_callback_number(self, number):
921                 try:
922                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
923                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
924                         elif number == self._backend.get_callback_number() and 0 < len(number):
925                                 _moduleLogger.warning(
926                                         "Callback number already is %s" % (
927                                                 self._backend.get_callback_number(),
928                                         ),
929                                 )
930                                 self._set_callback_label(number)
931                         else:
932                                 if number.startswith("1747"): number = "+" + number
933                                 self._backend.set_callback_number(number)
934                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
935                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
936                                 )
937                                 self._callbackNumber = make_ugly(number)
938                                 self._set_callback_label(number)
939                                 _moduleLogger.info(
940                                         "Callback number set to %s" % (
941                                                 self._backend.get_callback_number(),
942                                         ),
943                                 )
944                 except Exception, e:
945                         self._errorDisplay.push_exception()
946
947         def _set_callback_label(self, uglyNumber):
948                 prettyNumber = make_pretty(uglyNumber)
949                 if len(prettyNumber) == 0:
950                         prettyNumber = "No Callback Number"
951                 self._callbackSelectButton.set_label(prettyNumber)
952
953         def _update_alarm_settings(self, recurrence):
954                 try:
955                         isEnabled = self._notifyCheckbox.get_active()
956                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
957                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
958                 finally:
959                         self.save_everything()
960                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
961                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
962
963         def _on_callbackentry_clicked(self, *args):
964                 try:
965                         actualSelection = make_pretty(self._callbackNumber)
966
967                         userOptions = dict(
968                                 (number, "%s (%s)" % (number, description))
969                                 for (number, description) in self._callbackList
970                         )
971                         defaultSelection = userOptions.get(actualSelection, actualSelection)
972
973                         userSelection = hildonize.touch_selector_entry(
974                                 self._window,
975                                 "Callback Number",
976                                 list(userOptions.itervalues()),
977                                 defaultSelection,
978                         )
979                         reversedUserOptions = dict(
980                                 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
981                         )
982                         selectedNumber = reversedUserOptions.get(userSelection, userSelection)
983
984                         number = make_ugly(selectedNumber)
985                         self._set_callback_number(number)
986                 except RuntimeError, e:
987                         _moduleLogger.exception("%s" % str(e))
988                 except Exception, e:
989                         self._errorDisplay.push_exception()
990
991         def _on_notify_toggled(self, *args):
992                 try:
993                         if self._applyAlarmTimeoutId is not None:
994                                 gobject.source_remove(self._applyAlarmTimeoutId)
995                                 self._applyAlarmTimeoutId = None
996                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
997                 except Exception, e:
998                         self._errorDisplay.push_exception()
999
1000         def _on_minutes_clicked(self, *args):
1001                 recurrenceChoices = [
1002                         (1, "1 minute"),
1003                         (2, "2 minutes"),
1004                         (3, "3 minutes"),
1005                         (5, "5 minutes"),
1006                         (8, "8 minutes"),
1007                         (10, "10 minutes"),
1008                         (15, "15 minutes"),
1009                         (30, "30 minutes"),
1010                         (45, "45 minutes"),
1011                         (60, "1 hour"),
1012                         (3*60, "3 hours"),
1013                         (6*60, "6 hours"),
1014                         (12*60, "12 hours"),
1015                 ]
1016                 try:
1017                         actualSelection = self._alarmHandler.recurrence
1018
1019                         closestSelectionIndex = 0
1020                         for i, possible in enumerate(recurrenceChoices):
1021                                 if possible[0] <= actualSelection:
1022                                         closestSelectionIndex = i
1023                         recurrenceIndex = hildonize.touch_selector(
1024                                 self._window,
1025                                 "Minutes",
1026                                 (("%s" % m[1]) for m in recurrenceChoices),
1027                                 closestSelectionIndex,
1028                         )
1029                         recurrence = recurrenceChoices[recurrenceIndex][0]
1030
1031                         self._update_alarm_settings(recurrence)
1032                 except RuntimeError, e:
1033                         _moduleLogger.exception("%s" % str(e))
1034                 except Exception, e:
1035                         self._errorDisplay.push_exception()
1036
1037         def _on_apply_timeout(self, *args):
1038                 try:
1039                         self._applyAlarmTimeoutId = None
1040
1041                         self._update_alarm_settings(self._alarmHandler.recurrence)
1042                 except Exception, e:
1043                         self._errorDisplay.push_exception()
1044                 return False
1045
1046         def _on_missed_toggled(self, *args):
1047                 try:
1048                         self._notifyOnMissed = self._missedCheckbox.get_active()
1049                         self.save_everything()
1050                 except Exception, e:
1051                         self._errorDisplay.push_exception()
1052
1053         def _on_voicemail_toggled(self, *args):
1054                 try:
1055                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
1056                         self.save_everything()
1057                 except Exception, e:
1058                         self._errorDisplay.push_exception()
1059
1060         def _on_sms_toggled(self, *args):
1061                 try:
1062                         self._notifyOnSms = self._smsCheckbox.get_active()
1063                         self.save_everything()
1064                 except Exception, e:
1065                         self._errorDisplay.push_exception()
1066
1067
1068 class CallHistoryView(object):
1069
1070         NUMBER_IDX = 0
1071         DATE_IDX = 1
1072         ACTION_IDX = 2
1073         FROM_IDX = 3
1074         FROM_ID_IDX = 4
1075
1076         HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
1077
1078         def __init__(self, widgetTree, backend, errorDisplay):
1079                 self._errorDisplay = errorDisplay
1080                 self._backend = backend
1081
1082                 self._isPopulated = False
1083                 self._historymodel = gtk.ListStore(
1084                         gobject.TYPE_STRING, # number
1085                         gobject.TYPE_STRING, # date
1086                         gobject.TYPE_STRING, # action
1087                         gobject.TYPE_STRING, # from
1088                         gobject.TYPE_STRING, # from id
1089                 )
1090                 self._historymodelfiltered = self._historymodel.filter_new()
1091                 self._historymodelfiltered.set_visible_func(self._is_history_visible)
1092                 self._historyview = widgetTree.get_widget("historyview")
1093                 self._historyviewselection = None
1094                 self._onRecentviewRowActivatedId = 0
1095
1096                 textrenderer = gtk.CellRendererText()
1097                 textrenderer.set_property("yalign", 0)
1098                 self._dateColumn = gtk.TreeViewColumn("Date")
1099                 self._dateColumn.pack_start(textrenderer, expand=True)
1100                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
1101
1102                 textrenderer = gtk.CellRendererText()
1103                 textrenderer.set_property("yalign", 0)
1104                 self._actionColumn = gtk.TreeViewColumn("Action")
1105                 self._actionColumn.pack_start(textrenderer, expand=True)
1106                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
1107
1108                 textrenderer = gtk.CellRendererText()
1109                 textrenderer.set_property("yalign", 0)
1110                 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
1111                 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
1112                 self._numberColumn = gtk.TreeViewColumn("Number")
1113                 self._numberColumn.pack_start(textrenderer, expand=True)
1114                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
1115
1116                 textrenderer = gtk.CellRendererText()
1117                 textrenderer.set_property("yalign", 0)
1118                 hildonize.set_cell_thumb_selectable(textrenderer)
1119                 self._nameColumn = gtk.TreeViewColumn("From")
1120                 self._nameColumn.pack_start(textrenderer, expand=True)
1121                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
1122                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1123
1124                 self._window = gtk_toolbox.find_parent_window(self._historyview)
1125
1126                 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
1127                 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
1128                 self._selectedFilter = "All"
1129
1130                 self._updateSink = gtk_toolbox.threaded_stage(
1131                         gtk_toolbox.comap(
1132                                 self._idly_populate_historyview,
1133                                 gtk_toolbox.null_sink(),
1134                         )
1135                 )
1136
1137         def enable(self):
1138                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1139                 self._historyFilterSelector.set_label(self._selectedFilter)
1140
1141                 self._historyview.set_model(self._historymodelfiltered)
1142                 self._historyview.set_fixed_height_mode(False)
1143
1144                 self._historyview.append_column(self._dateColumn)
1145                 self._historyview.append_column(self._actionColumn)
1146                 self._historyview.append_column(self._numberColumn)
1147                 self._historyview.append_column(self._nameColumn)
1148                 self._historyviewselection = self._historyview.get_selection()
1149                 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
1150
1151                 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
1152
1153         def disable(self):
1154                 self._historyview.disconnect(self._onRecentviewRowActivatedId)
1155
1156                 self.clear()
1157
1158                 self._historyview.remove_column(self._dateColumn)
1159                 self._historyview.remove_column(self._actionColumn)
1160                 self._historyview.remove_column(self._nameColumn)
1161                 self._historyview.remove_column(self._numberColumn)
1162                 self._historyview.set_model(None)
1163
1164         def add_contact(self, *args, **kwds):
1165                 """
1166                 @note Actual dial function is patched in later
1167                 """
1168                 raise NotImplementedError("Horrible unknown error has occurred")
1169
1170         def update(self, force = False):
1171                 if not force and self._isPopulated:
1172                         return False
1173                 self._updateSink.send(())
1174                 return True
1175
1176         def clear(self):
1177                 self._isPopulated = False
1178                 self._historymodel.clear()
1179
1180         @staticmethod
1181         def name():
1182                 return "Recent Calls"
1183
1184         def load_settings(self, config, sectionName):
1185                 try:
1186                         self._selectedFilter = config.get(sectionName, "filter")
1187                         if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1188                                 self._messageType = self.HISTORY_ITEM_TYPES[0]
1189                 except ConfigParser.NoOptionError:
1190                         pass
1191
1192         def save_settings(self, config, sectionName):
1193                 """
1194                 @note Thread Agnostic
1195                 """
1196                 config.set(sectionName, "filter", self._selectedFilter)
1197
1198         def set_orientation(self, orientation):
1199                 if orientation == gtk.ORIENTATION_VERTICAL:
1200                         pass
1201                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1202                         pass
1203                 else:
1204                         raise NotImplementedError(orientation)
1205
1206         def _is_history_visible(self, model, iter):
1207                 try:
1208                         action = model.get_value(iter, self.ACTION_IDX)
1209                         if action is None:
1210                                 return False # this seems weird but oh well
1211
1212                         if self._selectedFilter in [action, "All"]:
1213                                 return True
1214                         else:
1215                                 return False
1216                 except Exception, e:
1217                         self._errorDisplay.push_exception()
1218
1219         def _idly_populate_historyview(self):
1220                 with gtk_toolbox.gtk_lock():
1221                         banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1222                 try:
1223                         self._historymodel.clear()
1224                         self._isPopulated = True
1225
1226                         try:
1227                                 historyItems = self._backend.get_recent()
1228                         except Exception, e:
1229                                 self._errorDisplay.push_exception_with_lock()
1230                                 self._isPopulated = False
1231                                 historyItems = []
1232
1233                         historyItems = (
1234                                 gv_backend.decorate_recent(data)
1235                                 for data in gv_backend.sort_messages(historyItems)
1236                         )
1237
1238                         for contactId, personName, phoneNumber, date, action in historyItems:
1239                                 if not personName:
1240                                         personName = "Unknown"
1241                                 date = abbrev_relative_date(date)
1242                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1243                                 prettyNumber = make_pretty(prettyNumber)
1244                                 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1245                                 with gtk_toolbox.gtk_lock():
1246                                         self._historymodel.append(item)
1247                 except Exception, e:
1248                         self._errorDisplay.push_exception_with_lock()
1249                 finally:
1250                         with gtk_toolbox.gtk_lock():
1251                                 hildonize.show_busy_banner_end(banner)
1252
1253                 return False
1254
1255         def _on_history_filter_clicked(self, *args, **kwds):
1256                 try:
1257                         selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1258
1259                         try:
1260                                 newSelectedComboIndex = hildonize.touch_selector(
1261                                         self._window,
1262                                         "History",
1263                                         self.HISTORY_ITEM_TYPES,
1264                                         selectedComboIndex,
1265                                 )
1266                         except RuntimeError:
1267                                 return
1268
1269                         option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1270                         self._selectedFilter = option
1271                         self._historyFilterSelector.set_label(self._selectedFilter)
1272                         self._historymodelfiltered.refilter()
1273                 except Exception, e:
1274                         self._errorDisplay.push_exception()
1275
1276         def _history_summary(self, expectedNumber):
1277                 for number, action, date, whoFrom, whoFromId in self._historymodel:
1278                         if expectedNumber is not None and expectedNumber == number:
1279                                 yield "%s <i>(%s)</i> - %s %s" % (number, whoFrom, date, action)
1280
1281         def _on_historyview_row_activated(self, treeview, path, view_column):
1282                 try:
1283                         childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1284                         itr = self._historymodel.get_iter(childPath)
1285                         if not itr:
1286                                 return
1287
1288                         prettyNumber = self._historymodel.get_value(itr, self.NUMBER_IDX)
1289                         number = make_ugly(prettyNumber)
1290                         description = list(self._history_summary(prettyNumber))
1291                         contactName = self._historymodel.get_value(itr, self.FROM_IDX)
1292                         contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1293                         contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1294
1295                         self.add_contact(
1296                                 contactName,
1297                                 contactPhoneNumbers,
1298                                 messages = description,
1299                                 defaultIndex = defaultIndex,
1300                         )
1301                         self._historyviewselection.unselect_all()
1302                 except Exception, e:
1303                         self._errorDisplay.push_exception()
1304
1305
1306 class MessagesView(object):
1307
1308         NUMBER_IDX = 0
1309         DATE_IDX = 1
1310         HEADER_IDX = 2
1311         MESSAGE_IDX = 3
1312         MESSAGES_IDX = 4
1313         FROM_ID_IDX = 5
1314         MESSAGE_DATA_IDX = 6
1315
1316         NO_MESSAGES = "None"
1317         VOICEMAIL_MESSAGES = "Voicemail"
1318         TEXT_MESSAGES = "SMS"
1319         ALL_TYPES = "All Messages"
1320         MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1321
1322         UNREAD_STATUS = "Unread"
1323         UNARCHIVED_STATUS = "Inbox"
1324         ALL_STATUS = "Any"
1325         MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1326
1327         def __init__(self, widgetTree, backend, errorDisplay):
1328                 self._errorDisplay = errorDisplay
1329                 self._backend = backend
1330
1331                 self._isPopulated = False
1332                 self._messagemodel = gtk.ListStore(
1333                         gobject.TYPE_STRING, # number
1334                         gobject.TYPE_STRING, # date
1335                         gobject.TYPE_STRING, # header
1336                         gobject.TYPE_STRING, # message
1337                         object, # messages
1338                         gobject.TYPE_STRING, # from id
1339                         object, # message data
1340                 )
1341                 self._messagemodelfiltered = self._messagemodel.filter_new()
1342                 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1343                 self._messageview = widgetTree.get_widget("messages_view")
1344                 self._messageviewselection = None
1345                 self._onMessageviewRowActivatedId = 0
1346
1347                 self._messageRenderer = gtk.CellRendererText()
1348                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1349                 self._messageRenderer.set_property("wrap-width", 500)
1350                 self._messageColumn = gtk.TreeViewColumn("Messages")
1351                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1352                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1353                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1354
1355                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1356
1357                 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1358                 self._onMessageTypeClickedId = 0
1359                 self._messageType = self.ALL_TYPES
1360                 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1361                 self._onMessageStatusClickedId = 0
1362                 self._messageStatus = self.ALL_STATUS
1363
1364                 self._updateSink = gtk_toolbox.threaded_stage(
1365                         gtk_toolbox.comap(
1366                                 self._idly_populate_messageview,
1367                                 gtk_toolbox.null_sink(),
1368                         )
1369                 )
1370
1371         def enable(self):
1372                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1373                 self._messageview.set_model(self._messagemodelfiltered)
1374                 self._messageview.set_headers_visible(False)
1375                 self._messageview.set_fixed_height_mode(False)
1376
1377                 self._messageview.append_column(self._messageColumn)
1378                 self._messageviewselection = self._messageview.get_selection()
1379                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1380
1381                 self._messageTypeButton.set_label(self._messageType)
1382                 self._messageStatusButton.set_label(self._messageStatus)
1383
1384                 self._onMessageviewRowActivatedId = self._messageview.connect(
1385                         "row-activated", self._on_messageview_row_activated
1386                 )
1387                 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1388                         "clicked", self._on_message_type_clicked
1389                 )
1390                 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1391                         "clicked", self._on_message_status_clicked
1392                 )
1393
1394         def disable(self):
1395                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1396                 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1397                 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1398
1399                 self.clear()
1400
1401                 self._messageview.remove_column(self._messageColumn)
1402                 self._messageview.set_model(None)
1403
1404         def add_contact(self, *args, **kwds):
1405                 """
1406                 @note Actual dial function is patched in later
1407                 """
1408                 raise NotImplementedError("Horrible unknown error has occurred")
1409
1410         def update(self, force = False):
1411                 if not force and self._isPopulated:
1412                         return False
1413                 self._updateSink.send(())
1414                 return True
1415
1416         def clear(self):
1417                 self._isPopulated = False
1418                 self._messagemodel.clear()
1419
1420         @staticmethod
1421         def name():
1422                 return "Messages"
1423
1424         def load_settings(self, config, sectionName):
1425                 try:
1426                         self._messageType = config.get(sectionName, "type")
1427                         if self._messageType not in self.MESSAGE_TYPES:
1428                                 self._messageType = self.ALL_TYPES
1429                         self._messageStatus = config.get(sectionName, "status")
1430                         if self._messageStatus not in self.MESSAGE_STATUSES:
1431                                 self._messageStatus = self.ALL_STATUS
1432                 except ConfigParser.NoOptionError:
1433                         pass
1434
1435         def save_settings(self, config, sectionName):
1436                 """
1437                 @note Thread Agnostic
1438                 """
1439                 config.set(sectionName, "status", self._messageStatus)
1440                 config.set(sectionName, "type", self._messageType)
1441
1442         def set_orientation(self, orientation):
1443                 if orientation == gtk.ORIENTATION_VERTICAL:
1444                         pass
1445                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1446                         pass
1447                 else:
1448                         raise NotImplementedError(orientation)
1449
1450         def _is_message_visible(self, model, iter):
1451                 try:
1452                         message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1453                         if message is None:
1454                                 return False # this seems weird but oh well
1455                         return self._filter_messages(message, self._messageType, self._messageStatus)
1456                 except Exception, e:
1457                         self._errorDisplay.push_exception()
1458
1459         @classmethod
1460         def _filter_messages(cls, message, type, status):
1461                 if type == cls.ALL_TYPES:
1462                         isType = True
1463                 else:
1464                         messageType = message["type"]
1465                         isType = messageType == type
1466
1467                 if status == cls.ALL_STATUS:
1468                         isStatus = True
1469                 else:
1470                         isUnarchived = not message["isArchived"]
1471                         isUnread = not message["isRead"]
1472                         if status == cls.UNREAD_STATUS:
1473                                 isStatus = isUnarchived and isUnread
1474                         elif status == cls.UNARCHIVED_STATUS:
1475                                 isStatus = isUnarchived
1476                         else:
1477                                 assert "Status %s is bad for %r" % (status, message)
1478
1479                 return isType and isStatus
1480
1481         _MIN_MESSAGES_SHOWN = 4
1482
1483         def _idly_populate_messageview(self):
1484                 with gtk_toolbox.gtk_lock():
1485                         banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1486                 try:
1487                         self._messagemodel.clear()
1488                         self._isPopulated = True
1489
1490                         if self._messageType == self.NO_MESSAGES:
1491                                 messageItems = []
1492                         else:
1493                                 try:
1494                                         messageItems = self._backend.get_messages()
1495                                 except Exception, e:
1496                                         self._errorDisplay.push_exception_with_lock()
1497                                         self._isPopulated = False
1498                                         messageItems = []
1499
1500                         messageItems = (
1501                                 (gv_backend.decorate_message(message), message)
1502                                 for message in gv_backend.sort_messages(messageItems)
1503                         )
1504
1505                         for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1506                                 prettyNumber = number[2:] if number.startswith("+1") else number
1507                                 prettyNumber = make_pretty(prettyNumber)
1508
1509                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1510                                 expandedMessages = [firstMessage]
1511                                 expandedMessages.extend(messages)
1512                                 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1513                                         firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1514                                         secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1515                                         collapsedMessages = [firstMessage, secondMessage]
1516                                         collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1517                                 else:
1518                                         collapsedMessages = expandedMessages
1519                                 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1520
1521                                 number = make_ugly(number)
1522
1523                                 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1524                                 with gtk_toolbox.gtk_lock():
1525                                         self._messagemodel.append(row)
1526                 except Exception, e:
1527                         self._errorDisplay.push_exception_with_lock()
1528                 finally:
1529                         with gtk_toolbox.gtk_lock():
1530                                 hildonize.show_busy_banner_end(banner)
1531                                 self._messagemodelfiltered.refilter()
1532
1533                 return False
1534
1535         def _on_messageview_row_activated(self, treeview, path, view_column):
1536                 try:
1537                         childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1538                         itr = self._messagemodel.get_iter(childPath)
1539                         if not itr:
1540                                 return
1541
1542                         number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1543                         description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1544
1545                         contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1546                         header = self._messagemodel.get_value(itr, self.HEADER_IDX)
1547                         contactPhoneNumbers, defaultIndex = _get_contact_numbers(self._backend, contactId, number)
1548
1549                         self.add_contact(
1550                                 header,
1551                                 contactPhoneNumbers,
1552                                 messages = description,
1553                                 defaultIndex = defaultIndex,
1554                         )
1555                         self._messageviewselection.unselect_all()
1556                 except Exception, e:
1557                         self._errorDisplay.push_exception()
1558
1559         def _on_message_type_clicked(self, *args, **kwds):
1560                 try:
1561                         selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1562
1563                         try:
1564                                 newSelectedIndex = hildonize.touch_selector(
1565                                         self._window,
1566                                         "Message Type",
1567                                         self.MESSAGE_TYPES,
1568                                         selectedIndex,
1569                                 )
1570                         except RuntimeError:
1571                                 return
1572
1573                         if selectedIndex != newSelectedIndex:
1574                                 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1575                                 self._messageTypeButton.set_label(self._messageType)
1576                                 self._messagemodelfiltered.refilter()
1577                 except Exception, e:
1578                         self._errorDisplay.push_exception()
1579
1580         def _on_message_status_clicked(self, *args, **kwds):
1581                 try:
1582                         selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1583
1584                         try:
1585                                 newSelectedIndex = hildonize.touch_selector(
1586                                         self._window,
1587                                         "Message Status",
1588                                         self.MESSAGE_STATUSES,
1589                                         selectedIndex,
1590                                 )
1591                         except RuntimeError:
1592                                 return
1593
1594                         if selectedIndex != newSelectedIndex:
1595                                 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1596                                 self._messageStatusButton.set_label(self._messageStatus)
1597                                 self._messagemodelfiltered.refilter()
1598                 except Exception, e:
1599                         self._errorDisplay.push_exception()
1600
1601
1602 class ContactsView(object):
1603
1604         CONTACT_TYPE_IDX = 0
1605         CONTACT_NAME_IDX = 1
1606         CONTACT_ID_IDX = 2
1607
1608         def __init__(self, widgetTree, backend, errorDisplay):
1609                 self._errorDisplay = errorDisplay
1610                 self._backend = backend
1611
1612                 self._addressBook = None
1613                 self._selectedComboIndex = 0
1614                 self._addressBookFactories = [null_backend.NullAddressBook()]
1615
1616                 self._booksList = []
1617                 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1618
1619                 self._isPopulated = False
1620                 self._contactsmodel = gtk.ListStore(
1621                         gobject.TYPE_STRING, # Contact Type
1622                         gobject.TYPE_STRING, # Contact Name
1623                         gobject.TYPE_STRING, # Contact ID
1624                 )
1625                 self._contactsviewselection = None
1626                 self._contactsview = widgetTree.get_widget("contactsview")
1627
1628                 self._contactColumn = gtk.TreeViewColumn("Contact")
1629                 displayContactSource = False
1630                 if displayContactSource:
1631                         textrenderer = gtk.CellRendererText()
1632                         self._contactColumn.pack_start(textrenderer, expand=False)
1633                         self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1634                 textrenderer = gtk.CellRendererText()
1635                 hildonize.set_cell_thumb_selectable(textrenderer)
1636                 self._contactColumn.pack_start(textrenderer, expand=True)
1637                 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1638                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1639                 self._contactColumn.set_sort_column_id(1)
1640                 self._contactColumn.set_visible(True)
1641
1642                 self._onContactsviewRowActivatedId = 0
1643                 self._onAddressbookButtonChangedId = 0
1644                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1645
1646                 self._updateSink = gtk_toolbox.threaded_stage(
1647                         gtk_toolbox.comap(
1648                                 self._idly_populate_contactsview,
1649                                 gtk_toolbox.null_sink(),
1650                         )
1651                 )
1652
1653         def enable(self):
1654                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1655
1656                 self._contactsview.set_model(self._contactsmodel)
1657                 self._contactsview.set_fixed_height_mode(False)
1658                 self._contactsview.append_column(self._contactColumn)
1659                 self._contactsviewselection = self._contactsview.get_selection()
1660                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1661
1662                 del self._booksList[:]
1663                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1664                         if factoryName and bookName:
1665                                 entryName = "%s: %s" % (factoryName, bookName)
1666                         elif factoryName:
1667                                 entryName = factoryName
1668                         elif bookName:
1669                                 entryName = bookName
1670                         else:
1671                                 entryName = "Bad name (%d)" % factoryId
1672                         row = (str(factoryId), bookId, entryName)
1673                         self._booksList.append(row)
1674
1675                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1676                 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1677
1678                 if len(self._booksList) <= self._selectedComboIndex:
1679                         self._selectedComboIndex = 0
1680                 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1681
1682                 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1683                 selectedBookId = self._booksList[self._selectedComboIndex][1]
1684                 self.open_addressbook(selectedFactoryId, selectedBookId)
1685
1686         def disable(self):
1687                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1688                 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1689
1690                 self.clear()
1691
1692                 self._bookSelectionButton.set_label("")
1693                 self._contactsview.set_model(None)
1694                 self._contactsview.remove_column(self._contactColumn)
1695
1696         def add_contact(self, *args, **kwds):
1697                 """
1698                 @note Actual dial function is patched in later
1699                 """
1700                 raise NotImplementedError("Horrible unknown error has occurred")
1701
1702         def get_addressbooks(self):
1703                 """
1704                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1705                 """
1706                 for i, factory in enumerate(self._addressBookFactories):
1707                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1708                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1709
1710         def open_addressbook(self, bookFactoryId, bookId):
1711                 bookFactoryIndex = int(bookFactoryId)
1712                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1713                 self._addressBook = addressBook
1714
1715         def update(self, force = False):
1716                 if not force and self._isPopulated:
1717                         return False
1718                 self._updateSink.send(())
1719                 return True
1720
1721         def clear(self):
1722                 self._isPopulated = False
1723                 self._contactsmodel.clear()
1724                 for factory in self._addressBookFactories:
1725                         factory.clear_caches()
1726                 self._addressBook.clear_caches()
1727
1728         def append(self, book):
1729                 self._addressBookFactories.append(book)
1730
1731         def extend(self, books):
1732                 self._addressBookFactories.extend(books)
1733
1734         @staticmethod
1735         def name():
1736                 return "Contacts"
1737
1738         def load_settings(self, config, sectionName):
1739                 try:
1740                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1741                 except ConfigParser.NoOptionError:
1742                         self._selectedComboIndex = 0
1743
1744         def save_settings(self, config, sectionName):
1745                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1746
1747         def set_orientation(self, orientation):
1748                 if orientation == gtk.ORIENTATION_VERTICAL:
1749                         pass
1750                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
1751                         pass
1752                 else:
1753                         raise NotImplementedError(orientation)
1754
1755         def _idly_populate_contactsview(self):
1756                 with gtk_toolbox.gtk_lock():
1757                         banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1758                 try:
1759                         addressBook = None
1760                         while addressBook is not self._addressBook:
1761                                 addressBook = self._addressBook
1762                                 with gtk_toolbox.gtk_lock():
1763                                         self._contactsview.set_model(None)
1764                                         self.clear()
1765
1766                                 try:
1767                                         contacts = addressBook.get_contacts()
1768                                 except Exception, e:
1769                                         contacts = []
1770                                         self._isPopulated = False
1771                                         self._errorDisplay.push_exception_with_lock()
1772                                 for contactId, contactName in contacts:
1773                                         contactType = addressBook.contact_source_short_name(contactId)
1774                                         row = contactType, contactName, contactId
1775                                         self._contactsmodel.append(row)
1776
1777                                 with gtk_toolbox.gtk_lock():
1778                                         self._contactsview.set_model(self._contactsmodel)
1779
1780                         self._isPopulated = True
1781                 except Exception, e:
1782                         self._errorDisplay.push_exception_with_lock()
1783                 finally:
1784                         with gtk_toolbox.gtk_lock():
1785                                 hildonize.show_busy_banner_end(banner)
1786                 return False
1787
1788         def _on_addressbook_button_changed(self, *args, **kwds):
1789                 try:
1790                         try:
1791                                 newSelectedComboIndex = hildonize.touch_selector(
1792                                         self._window,
1793                                         "Addressbook",
1794                                         (("%s" % m[2]) for m in self._booksList),
1795                                         self._selectedComboIndex,
1796                                 )
1797                         except RuntimeError:
1798                                 return
1799
1800                         selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1801                         selectedBookId = self._booksList[newSelectedComboIndex][1]
1802
1803                         oldAddressbook = self._addressBook
1804                         self.open_addressbook(selectedFactoryId, selectedBookId)
1805                         forceUpdate = True if oldAddressbook is not self._addressBook else False
1806                         self.update(force=forceUpdate)
1807
1808                         self._selectedComboIndex = newSelectedComboIndex
1809                         self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1810                 except Exception, e:
1811                         self._errorDisplay.push_exception()
1812
1813         def _on_contactsview_row_activated(self, treeview, path, view_column):
1814                 try:
1815                         itr = self._contactsmodel.get_iter(path)
1816                         if not itr:
1817                                 return
1818
1819                         contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1820                         contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1821                         try:
1822                                 contactDetails = self._addressBook.get_contact_details(contactId)
1823                         except Exception, e:
1824                                 contactDetails = []
1825                                 self._errorDisplay.push_exception()
1826                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1827
1828                         if len(contactPhoneNumbers) == 0:
1829                                 return
1830
1831                         self.add_contact(
1832                                 contactName,
1833                                 contactPhoneNumbers,
1834                                 messages = (contactName, ),
1835                         )
1836                         self._contactsviewselection.unselect_all()
1837                 except Exception, e:
1838                         self._errorDisplay.push_exception()