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