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