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