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