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