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