Fixing the dialpad
[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._okButton = widgetTree.get_widget("dialpadOk")
489                 self._backButton = widgetTree.get_widget("back")
490                 self._plusButton = widgetTree.get_widget("plus")
491                 self._phonenumber = ""
492                 self._prettynumber = ""
493
494                 callbackMapping = {
495                         "on_digit_clicked": self._on_digit_clicked,
496                 }
497                 widgetTree.signal_autoconnect(callbackMapping)
498                 self._okButton.connect("clicked", self._on_ok_clicked)
499                 self._plusButton.connect("clicked", self._on_plus)
500
501                 self._originalLabel = self._backButton.get_label()
502                 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
503                 self._backTapHandler.on_tap = self._on_backspace
504                 self._backTapHandler.on_hold = self._on_clearall
505                 self._backTapHandler.on_holding = self._set_clear_button
506                 self._backTapHandler.on_cancel = self._reset_back_button
507
508                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
509                 self._keyPressEventId = 0
510
511         def enable(self):
512                 self._okButton.grab_focus()
513                 self._backTapHandler.enable()
514                 self._keyPressEventId = self._window.connect("key-press-event", self._on_key_press)
515
516         def disable(self):
517                 self._window.disconnect(self._keyPressEventId)
518                 self._keyPressEventId = 0
519                 self._reset_back_button()
520                 self._backTapHandler.disable()
521
522         def add_contact(self, *args, **kwds):
523                 """
524                 @note Actual dial function is patched in later
525                 """
526                 raise NotImplementedError("Horrible unknown error has occurred")
527
528         def get_number(self):
529                 return self._phonenumber
530
531         def set_number(self, number):
532                 """
533                 Set the number to dial
534                 """
535                 try:
536                         self._phonenumber = make_ugly(number)
537                         self._prettynumber = make_pretty(self._phonenumber)
538                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
539                 except TypeError, e:
540                         self._errorDisplay.push_exception()
541
542         def clear(self):
543                 self.set_number("")
544
545         @staticmethod
546         def name():
547                 return "Dialpad"
548
549         def load_settings(self, config, section):
550                 pass
551
552         def save_settings(self, config, section):
553                 """
554                 @note Thread Agnostic
555                 """
556                 pass
557
558         def _on_key_press(self, widget, event):
559                 try:
560                         if event.keyval == ord("v") and event.get_state() & gtk.gdk.CONTROL_MASK:
561                                 contents = self._clipboard.wait_for_text()
562                                 if contents is not None:
563                                         self.set_number(contents)
564                 except Exception, e:
565                         self._errorDisplay.push_exception()
566
567         def _on_ok_clicked(self, widget):
568                 try:
569                         phoneNumber = self.get_number()
570                         self.add_contact(
571                                 [("Dialer", phoneNumber)], (), self._window
572                         )
573                 except Exception, e:
574                         self._errorDisplay.push_exception()
575
576         def _on_digit_clicked(self, widget):
577                 try:
578                         self.set_number(self._phonenumber + widget.get_name()[-1])
579                 except Exception, e:
580                         self._errorDisplay.push_exception()
581
582         def _on_plus(self, *args):
583                 try:
584                         self.set_number(self._phonenumber + "+")
585                 except Exception, e:
586                         self._errorDisplay.push_exception()
587
588         def _on_backspace(self, taps):
589                 try:
590                         self.set_number(self._phonenumber[:-taps])
591                         self._reset_back_button()
592                 except Exception, e:
593                         self._errorDisplay.push_exception()
594
595         def _on_clearall(self, taps):
596                 try:
597                         self.clear()
598                         self._reset_back_button()
599                 except Exception, e:
600                         self._errorDisplay.push_exception()
601                 return False
602
603         def _set_clear_button(self):
604                 try:
605                         self._backButton.set_label("gtk-clear")
606                 except Exception, e:
607                         self._errorDisplay.push_exception()
608
609         def _reset_back_button(self):
610                 try:
611                         self._backButton.set_label(self._originalLabel)
612                 except Exception, e:
613                         self._errorDisplay.push_exception()
614
615
616 class AccountInfo(object):
617
618         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
619                 self._errorDisplay = errorDisplay
620                 self._backend = backend
621                 self._isPopulated = False
622                 self._alarmHandler = alarmHandler
623                 self._notifyOnMissed = False
624                 self._notifyOnVoicemail = False
625                 self._notifyOnSms = False
626
627                 self._callbackList = []
628                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
629                 self._callbackSelectButton = widgetTree.get_widget("callbackSelectButton")
630                 self._onCallbackSelectChangedId = 0
631
632                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
633                 self._minutesEntryButton = widgetTree.get_widget("minutesEntryButton")
634                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
635                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
636                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
637                 self._onNotifyToggled = 0
638                 self._onMinutesChanged = 0
639                 self._onMissedToggled = 0
640                 self._onVoicemailToggled = 0
641                 self._onSmsToggled = 0
642                 self._applyAlarmTimeoutId = None
643
644                 self._window = gtk_toolbox.find_parent_window(self._minutesEntryButton)
645                 self._callbackNumber = ""
646
647         def enable(self):
648                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
649
650                 self._accountViewNumberDisplay.set_use_markup(True)
651                 self.set_account_number("")
652
653                 del self._callbackList[:]
654                 self._onCallbackSelectChangedId = self._callbackSelectButton.connect("clicked", self._on_callbackentry_clicked)
655                 self._set_callback_label("")
656
657                 if self._alarmHandler is not None:
658                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
659                         self._minutesEntryButton.set_label("%d minutes" % self._alarmHandler.recurrence)
660                         self._missedCheckbox.set_active(self._notifyOnMissed)
661                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
662                         self._smsCheckbox.set_active(self._notifyOnSms)
663
664                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
665                         self._onMinutesChanged = self._minutesEntryButton.connect("clicked", self._on_minutes_clicked)
666                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
667                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
668                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
669                 else:
670                         self._notifyCheckbox.set_sensitive(False)
671                         self._minutesEntryButton.set_sensitive(False)
672                         self._missedCheckbox.set_sensitive(False)
673                         self._voicemailCheckbox.set_sensitive(False)
674                         self._smsCheckbox.set_sensitive(False)
675
676                 self.update(force=True)
677
678         def disable(self):
679                 self._callbackSelectButton.disconnect(self._onCallbackSelectChangedId)
680                 self._onCallbackSelectChangedId = 0
681                 self._set_callback_label("")
682
683                 if self._alarmHandler is not None:
684                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
685                         self._minutesEntryButton.disconnect(self._onMinutesChanged)
686                         self._missedCheckbox.disconnect(self._onNotifyToggled)
687                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
688                         self._smsCheckbox.disconnect(self._onNotifyToggled)
689                         self._onNotifyToggled = 0
690                         self._onMinutesChanged = 0
691                         self._onMissedToggled = 0
692                         self._onVoicemailToggled = 0
693                         self._onSmsToggled = 0
694                 else:
695                         self._notifyCheckbox.set_sensitive(True)
696                         self._minutesEntryButton.set_sensitive(True)
697                         self._missedCheckbox.set_sensitive(True)
698                         self._voicemailCheckbox.set_sensitive(True)
699                         self._smsCheckbox.set_sensitive(True)
700
701                 self.clear()
702                 del self._callbackList[:]
703
704         def set_account_number(self, number):
705                 """
706                 Displays current account number
707                 """
708                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
709
710         def update(self, force = False):
711                 if not force and self._isPopulated:
712                         return False
713                 self._populate_callback_combo()
714                 self.set_account_number(self._backend.get_account_number())
715                 return True
716
717         def clear(self):
718                 self._set_callback_label("")
719                 self.set_account_number("")
720                 self._isPopulated = False
721
722         def save_everything(self):
723                 raise NotImplementedError
724
725         @staticmethod
726         def name():
727                 return "Account Info"
728
729         def load_settings(self, config, section):
730                 self._callbackNumber = make_ugly(config.get(section, "callback"))
731                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
732                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
733                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
734
735         def save_settings(self, config, section):
736                 """
737                 @note Thread Agnostic
738                 """
739                 config.set(section, "callback", self._callbackNumber)
740                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
741                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
742                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
743
744         def _populate_callback_combo(self):
745                 self._isPopulated = True
746                 del self._callbackList[:]
747                 try:
748                         callbackNumbers = self._backend.get_callback_numbers()
749                 except Exception, e:
750                         self._errorDisplay.push_exception()
751                         self._isPopulated = False
752                         return
753
754                 if len(callbackNumbers) == 0:
755                         callbackNumbers = {"": "No callback numbers available"}
756
757                 for number, description in callbackNumbers.iteritems():
758                         self._callbackList.append((make_pretty(number), description))
759
760                 self._set_callback_number(self._callbackNumber)
761
762         def _set_callback_number(self, number):
763                 try:
764                         if not self._backend.is_valid_syntax(number) and 0 < len(number):
765                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
766                         elif number == self._backend.get_callback_number() and 0 < len(number):
767                                 _moduleLogger.warning(
768                                         "Callback number already is %s" % (
769                                                 self._backend.get_callback_number(),
770                                         ),
771                                 )
772                                 self._set_callback_label(number)
773                         else:
774                                 if number.startswith("1747"): number = "+" + number
775                                 self._backend.set_callback_number(number)
776                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
777                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
778                                 )
779                                 self._callbackNumber = make_ugly(number)
780                                 self._set_callback_label(number)
781                                 _moduleLogger.info(
782                                         "Callback number set to %s" % (
783                                                 self._backend.get_callback_number(),
784                                         ),
785                                 )
786                 except Exception, e:
787                         self._errorDisplay.push_exception()
788
789         def _set_callback_label(self, uglyNumber):
790                 prettyNumber = make_pretty(uglyNumber)
791                 if len(prettyNumber) == 0:
792                         prettyNumber = "No Callback Number"
793                 self._callbackSelectButton.set_label(prettyNumber)
794
795         def _update_alarm_settings(self, recurrence):
796                 try:
797                         isEnabled = self._notifyCheckbox.get_active()
798                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
799                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
800                 finally:
801                         self.save_everything()
802                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
803                         self._minutesEntryButton.set_label("%d Minutes" % self._alarmHandler.recurrence)
804
805         def _on_callbackentry_clicked(self, *args):
806                 try:
807                         actualSelection = make_pretty(self._callbackNumber)
808
809                         userOptions = dict(
810                                 (number, "%s (%s)" % (number, description))
811                                 for (number, description) in self._callbackList
812                         )
813                         defaultSelection = userOptions.get(actualSelection, actualSelection)
814
815                         userSelection = hildonize.touch_selector_entry(
816                                 self._window,
817                                 "Callback Number",
818                                 list(userOptions.itervalues()),
819                                 defaultSelection,
820                         )
821                         reversedUserOptions = dict(
822                                 itertools.izip(userOptions.itervalues(), userOptions.iterkeys())
823                         )
824                         selectedNumber = reversedUserOptions.get(userSelection, userSelection)
825
826                         number = make_ugly(selectedNumber)
827                         self._set_callback_number(number)
828                 except RuntimeError, e:
829                         _moduleLogger.exception("%s" % str(e))
830                 except Exception, e:
831                         self._errorDisplay.push_exception()
832
833         def _on_notify_toggled(self, *args):
834                 try:
835                         if self._applyAlarmTimeoutId is not None:
836                                 gobject.source_remove(self._applyAlarmTimeoutId)
837                                 self._applyAlarmTimeoutId = None
838                         self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
839                 except Exception, e:
840                         self._errorDisplay.push_exception()
841
842         def _on_minutes_clicked(self, *args):
843                 recurrenceChoices = [
844                         (1, "1 minute"),
845                         (2, "2 minutes"),
846                         (3, "3 minutes"),
847                         (5, "5 minutes"),
848                         (8, "8 minutes"),
849                         (10, "10 minutes"),
850                         (15, "15 minutes"),
851                         (30, "30 minutes"),
852                         (45, "45 minutes"),
853                         (60, "1 hour"),
854                         (3*60, "3 hours"),
855                         (6*60, "6 hours"),
856                         (12*60, "12 hours"),
857                 ]
858                 try:
859                         actualSelection = self._alarmHandler.recurrence
860
861                         closestSelectionIndex = 0
862                         for i, possible in enumerate(recurrenceChoices):
863                                 if possible[0] <= actualSelection:
864                                         closestSelectionIndex = i
865                         recurrenceIndex = hildonize.touch_selector(
866                                 self._window,
867                                 "Minutes",
868                                 (("%s" % m[1]) for m in recurrenceChoices),
869                                 closestSelectionIndex,
870                         )
871                         recurrence = recurrenceChoices[recurrenceIndex][0]
872
873                         self._update_alarm_settings(recurrence)
874                 except RuntimeError, e:
875                         _moduleLogger.exception("%s" % str(e))
876                 except Exception, e:
877                         self._errorDisplay.push_exception()
878
879         def _on_apply_timeout(self, *args):
880                 try:
881                         self._applyAlarmTimeoutId = None
882
883                         self._update_alarm_settings(self._alarmHandler.recurrence)
884                 except Exception, e:
885                         self._errorDisplay.push_exception()
886                 return False
887
888         def _on_missed_toggled(self, *args):
889                 try:
890                         self._notifyOnMissed = self._missedCheckbox.get_active()
891                         self.save_everything()
892                 except Exception, e:
893                         self._errorDisplay.push_exception()
894
895         def _on_voicemail_toggled(self, *args):
896                 try:
897                         self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
898                         self.save_everything()
899                 except Exception, e:
900                         self._errorDisplay.push_exception()
901
902         def _on_sms_toggled(self, *args):
903                 try:
904                         self._notifyOnSms = self._smsCheckbox.get_active()
905                         self.save_everything()
906                 except Exception, e:
907                         self._errorDisplay.push_exception()
908
909
910 class CallHistoryView(object):
911
912         NUMBER_IDX = 0
913         DATE_IDX = 1
914         ACTION_IDX = 2
915         FROM_IDX = 3
916         FROM_ID_IDX = 4
917
918         HISTORY_ITEM_TYPES = ["All", "Received", "Missed", "Placed"]
919
920         def __init__(self, widgetTree, backend, errorDisplay):
921                 self._errorDisplay = errorDisplay
922                 self._backend = backend
923
924                 self._isPopulated = False
925                 self._historymodel = gtk.ListStore(
926                         gobject.TYPE_STRING, # number
927                         gobject.TYPE_STRING, # date
928                         gobject.TYPE_STRING, # action
929                         gobject.TYPE_STRING, # from
930                         gobject.TYPE_STRING, # from id
931                 )
932                 self._historymodelfiltered = self._historymodel.filter_new()
933                 self._historymodelfiltered.set_visible_func(self._is_history_visible)
934                 self._historyview = widgetTree.get_widget("historyview")
935                 self._historyviewselection = None
936                 self._onRecentviewRowActivatedId = 0
937
938                 textrenderer = gtk.CellRendererText()
939                 textrenderer.set_property("yalign", 0)
940                 self._dateColumn = gtk.TreeViewColumn("Date")
941                 self._dateColumn.pack_start(textrenderer, expand=True)
942                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
943
944                 textrenderer = gtk.CellRendererText()
945                 textrenderer.set_property("yalign", 0)
946                 self._actionColumn = gtk.TreeViewColumn("Action")
947                 self._actionColumn.pack_start(textrenderer, expand=True)
948                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
949
950                 textrenderer = gtk.CellRendererText()
951                 textrenderer.set_property("yalign", 0)
952                 textrenderer.set_property("ellipsize", pango.ELLIPSIZE_END)
953                 textrenderer.set_property("width-chars", len("1 (555) 555-1234"))
954                 self._numberColumn = gtk.TreeViewColumn("Number")
955                 self._numberColumn.pack_start(textrenderer, expand=True)
956                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
957
958                 textrenderer = gtk.CellRendererText()
959                 textrenderer.set_property("yalign", 0)
960                 hildonize.set_cell_thumb_selectable(textrenderer)
961                 self._nameColumn = gtk.TreeViewColumn("From")
962                 self._nameColumn.pack_start(textrenderer, expand=True)
963                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
964                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
965
966                 self._window = gtk_toolbox.find_parent_window(self._historyview)
967
968                 self._historyFilterSelector = widgetTree.get_widget("historyFilterSelector")
969                 self._historyFilterSelector.connect("clicked", self._on_history_filter_clicked)
970                 self._selectedFilter = "All"
971
972                 self._updateSink = gtk_toolbox.threaded_stage(
973                         gtk_toolbox.comap(
974                                 self._idly_populate_historyview,
975                                 gtk_toolbox.null_sink(),
976                         )
977                 )
978
979         def enable(self):
980                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
981                 self._historyFilterSelector.set_label(self._selectedFilter)
982
983                 self._historyview.set_model(self._historymodelfiltered)
984                 self._historyview.set_fixed_height_mode(False)
985
986                 self._historyview.append_column(self._dateColumn)
987                 self._historyview.append_column(self._actionColumn)
988                 self._historyview.append_column(self._numberColumn)
989                 self._historyview.append_column(self._nameColumn)
990                 self._historyviewselection = self._historyview.get_selection()
991                 self._historyviewselection.set_mode(gtk.SELECTION_SINGLE)
992
993                 self._onRecentviewRowActivatedId = self._historyview.connect("row-activated", self._on_historyview_row_activated)
994
995         def disable(self):
996                 self._historyview.disconnect(self._onRecentviewRowActivatedId)
997
998                 self.clear()
999
1000                 self._historyview.remove_column(self._dateColumn)
1001                 self._historyview.remove_column(self._actionColumn)
1002                 self._historyview.remove_column(self._nameColumn)
1003                 self._historyview.remove_column(self._numberColumn)
1004                 self._historyview.set_model(None)
1005
1006         def add_contact(self, *args, **kwds):
1007                 """
1008                 @note Actual dial function is patched in later
1009                 """
1010                 raise NotImplementedError("Horrible unknown error has occurred")
1011
1012         def update(self, force = False):
1013                 if not force and self._isPopulated:
1014                         return False
1015                 self._updateSink.send(())
1016                 return True
1017
1018         def clear(self):
1019                 self._isPopulated = False
1020                 self._historymodel.clear()
1021
1022         @staticmethod
1023         def name():
1024                 return "Recent Calls"
1025
1026         def load_settings(self, config, sectionName):
1027                 try:
1028                         self._selectedFilter = config.get(sectionName, "filter")
1029                         if self._selectedFilter not in self.HISTORY_ITEM_TYPES:
1030                                 self._messageType = self.HISTORY_ITEM_TYPES[0]
1031                 except ConfigParser.NoOptionError:
1032                         pass
1033
1034         def save_settings(self, config, sectionName):
1035                 """
1036                 @note Thread Agnostic
1037                 """
1038                 config.set(sectionName, "filter", self._selectedFilter)
1039
1040         def _is_history_visible(self, model, iter):
1041                 try:
1042                         action = model.get_value(iter, self.ACTION_IDX)
1043                         if action is None:
1044                                 return False # this seems weird but oh well
1045
1046                         if self._selectedFilter in [action, "All"]:
1047                                 return True
1048                         else:
1049                                 return False
1050                 except Exception, e:
1051                         self._errorDisplay.push_exception()
1052
1053         def _idly_populate_historyview(self):
1054                 with gtk_toolbox.gtk_lock():
1055                         banner = hildonize.show_busy_banner_start(self._window, "Loading Call History")
1056                 try:
1057                         self._historymodel.clear()
1058                         self._isPopulated = True
1059
1060                         try:
1061                                 historyItems = self._backend.get_recent()
1062                         except Exception, e:
1063                                 self._errorDisplay.push_exception_with_lock()
1064                                 self._isPopulated = False
1065                                 historyItems = []
1066
1067                         historyItems = (
1068                                 gv_backend.decorate_recent(data)
1069                                 for data in gv_backend.sort_messages(historyItems)
1070                         )
1071
1072                         for contactId, personName, phoneNumber, date, action in historyItems:
1073                                 if not personName:
1074                                         personName = "Unknown"
1075                                 date = abbrev_relative_date(date)
1076                                 prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
1077                                 prettyNumber = make_pretty(prettyNumber)
1078                                 item = (prettyNumber, date, action.capitalize(), personName, contactId)
1079                                 with gtk_toolbox.gtk_lock():
1080                                         self._historymodel.append(item)
1081                 except Exception, e:
1082                         self._errorDisplay.push_exception_with_lock()
1083                 finally:
1084                         with gtk_toolbox.gtk_lock():
1085                                 hildonize.show_busy_banner_end(banner)
1086
1087                 return False
1088
1089         def _on_history_filter_clicked(self, *args, **kwds):
1090                 try:
1091                         selectedComboIndex = self.HISTORY_ITEM_TYPES.index(self._selectedFilter)
1092
1093                         try:
1094                                 newSelectedComboIndex = hildonize.touch_selector(
1095                                         self._window,
1096                                         "History",
1097                                         self.HISTORY_ITEM_TYPES,
1098                                         selectedComboIndex,
1099                                 )
1100                         except RuntimeError:
1101                                 return
1102
1103                         option = self.HISTORY_ITEM_TYPES[newSelectedComboIndex]
1104                         self._selectedFilter = option
1105                         self._historyFilterSelector.set_label(self._selectedFilter)
1106                         self._historymodelfiltered.refilter()
1107                 except Exception, e:
1108                         self._errorDisplay.push_exception()
1109
1110         def _on_historyview_row_activated(self, treeview, path, view_column):
1111                 try:
1112                         childPath = self._historymodelfiltered.convert_path_to_child_path(path)
1113                         itr = self._historymodel.get_iter(childPath)
1114                         if not itr:
1115                                 return
1116
1117                         number = self._historymodel.get_value(itr, self.NUMBER_IDX)
1118                         number = make_ugly(number)
1119                         description = self._historymodel.get_value(itr, self.FROM_IDX)
1120                         contactId = self._historymodel.get_value(itr, self.FROM_ID_IDX)
1121                         if contactId:
1122                                 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1123                                 defaultMatches = [
1124                                         (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1125                                         for (numberDescription, contactNumber) in contactPhoneNumbers
1126                                 ]
1127                                 try:
1128                                         defaultIndex = defaultMatches.index(True)
1129                                 except ValueError:
1130                                         contactPhoneNumbers.append(("Other", number))
1131                                         defaultIndex = len(contactPhoneNumbers)-1
1132                                         _moduleLogger.warn(
1133                                                 "Could not find contact %r's number %s among %r" % (
1134                                                         contactId, number, contactPhoneNumbers
1135                                                 )
1136                                         )
1137                         else:
1138                                 contactPhoneNumbers = [("Phone", number)]
1139                                 defaultIndex = -1
1140
1141                         self.add_contact(
1142                                 contactPhoneNumbers,
1143                                 messages = (description, ),
1144                                 parent = self._window,
1145                                 defaultIndex = defaultIndex,
1146                         )
1147                         self._historyviewselection.unselect_all()
1148                 except Exception, e:
1149                         self._errorDisplay.push_exception()
1150
1151
1152 class MessagesView(object):
1153
1154         NUMBER_IDX = 0
1155         DATE_IDX = 1
1156         HEADER_IDX = 2
1157         MESSAGE_IDX = 3
1158         MESSAGES_IDX = 4
1159         FROM_ID_IDX = 5
1160         MESSAGE_DATA_IDX = 6
1161
1162         NO_MESSAGES = "None"
1163         VOICEMAIL_MESSAGES = "Voicemail"
1164         TEXT_MESSAGES = "Texts"
1165         ALL_TYPES = "All Messages"
1166         MESSAGE_TYPES = [NO_MESSAGES, VOICEMAIL_MESSAGES, TEXT_MESSAGES, ALL_TYPES]
1167
1168         UNREAD_STATUS = "Unread"
1169         UNARCHIVED_STATUS = "Inbox"
1170         ALL_STATUS = "Any"
1171         MESSAGE_STATUSES = [UNREAD_STATUS, UNARCHIVED_STATUS, ALL_STATUS]
1172
1173         def __init__(self, widgetTree, backend, errorDisplay):
1174                 self._errorDisplay = errorDisplay
1175                 self._backend = backend
1176
1177                 self._isPopulated = False
1178                 self._messagemodel = gtk.ListStore(
1179                         gobject.TYPE_STRING, # number
1180                         gobject.TYPE_STRING, # date
1181                         gobject.TYPE_STRING, # header
1182                         gobject.TYPE_STRING, # message
1183                         object, # messages
1184                         gobject.TYPE_STRING, # from id
1185                         object, # message data
1186                 )
1187                 self._messagemodelfiltered = self._messagemodel.filter_new()
1188                 self._messagemodelfiltered.set_visible_func(self._is_message_visible)
1189                 self._messageview = widgetTree.get_widget("messages_view")
1190                 self._messageviewselection = None
1191                 self._onMessageviewRowActivatedId = 0
1192
1193                 self._messageRenderer = gtk.CellRendererText()
1194                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
1195                 self._messageRenderer.set_property("wrap-width", 500)
1196                 self._messageColumn = gtk.TreeViewColumn("Messages")
1197                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
1198                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
1199                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1200
1201                 self._window = gtk_toolbox.find_parent_window(self._messageview)
1202
1203                 self._messageTypeButton = widgetTree.get_widget("messageTypeButton")
1204                 self._onMessageTypeClickedId = 0
1205                 self._messageType = self.ALL_TYPES
1206                 self._messageStatusButton = widgetTree.get_widget("messageStatusButton")
1207                 self._onMessageStatusClickedId = 0
1208                 self._messageStatus = self.ALL_STATUS
1209
1210                 self._updateSink = gtk_toolbox.threaded_stage(
1211                         gtk_toolbox.comap(
1212                                 self._idly_populate_messageview,
1213                                 gtk_toolbox.null_sink(),
1214                         )
1215                 )
1216
1217         def enable(self):
1218                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1219                 self._messageview.set_model(self._messagemodelfiltered)
1220                 self._messageview.set_headers_visible(False)
1221                 self._messageview.set_fixed_height_mode(False)
1222
1223                 self._messageview.append_column(self._messageColumn)
1224                 self._messageviewselection = self._messageview.get_selection()
1225                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1226
1227                 self._messageTypeButton.set_label(self._messageType)
1228                 self._messageStatusButton.set_label(self._messageStatus)
1229
1230                 self._onMessageviewRowActivatedId = self._messageview.connect(
1231                         "row-activated", self._on_messageview_row_activated
1232                 )
1233                 self._onMessageTypeClickedId = self._messageTypeButton.connect(
1234                         "clicked", self._on_message_type_clicked
1235                 )
1236                 self._onMessageStatusClickedId = self._messageStatusButton.connect(
1237                         "clicked", self._on_message_status_clicked
1238                 )
1239
1240         def disable(self):
1241                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1242                 self._messageTypeButton.disconnect(self._onMessageTypeClickedId)
1243                 self._messageStatusButton.disconnect(self._onMessageStatusClickedId)
1244
1245                 self.clear()
1246
1247                 self._messageview.remove_column(self._messageColumn)
1248                 self._messageview.set_model(None)
1249
1250         def add_contact(self, *args, **kwds):
1251                 """
1252                 @note Actual dial function is patched in later
1253                 """
1254                 raise NotImplementedError("Horrible unknown error has occurred")
1255
1256         def update(self, force = False):
1257                 if not force and self._isPopulated:
1258                         return False
1259                 self._updateSink.send(())
1260                 return True
1261
1262         def clear(self):
1263                 self._isPopulated = False
1264                 self._messagemodel.clear()
1265
1266         @staticmethod
1267         def name():
1268                 return "Messages"
1269
1270         def load_settings(self, config, sectionName):
1271                 try:
1272                         self._messageType = config.get(sectionName, "type")
1273                         if self._messageType not in self.MESSAGE_TYPES:
1274                                 self._messageType = self.ALL_TYPES
1275                         self._messageStatus = config.get(sectionName, "status")
1276                         if self._messageStatus not in self.MESSAGE_STATUSES:
1277                                 self._messageStatus = self.ALL_STATUS
1278                 except ConfigParser.NoOptionError:
1279                         pass
1280
1281         def save_settings(self, config, sectionName):
1282                 """
1283                 @note Thread Agnostic
1284                 """
1285                 config.set(sectionName, "status", self._messageStatus)
1286                 config.set(sectionName, "type", self._messageType)
1287
1288         def _is_message_visible(self, model, iter):
1289                 try:
1290                         message = model.get_value(iter, self.MESSAGE_DATA_IDX)
1291                         if message is None:
1292                                 return False # this seems weird but oh well
1293                         return self._filter_messages(message, self._messageType, self._messageStatus)
1294                 except Exception, e:
1295                         self._errorDisplay.push_exception()
1296
1297         @classmethod
1298         def _filter_messages(cls, message, type, status):
1299                 if type == cls.ALL_TYPES:
1300                         isType = True
1301                 else:
1302                         messageType = message["type"]
1303                         isType = messageType == type
1304
1305                 if status == cls.ALL_STATUS:
1306                         isStatus = True
1307                 else:
1308                         isUnarchived = not message["isArchived"]
1309                         isUnread = not message["isRead"]
1310                         if status == cls.UNREAD_STATUS:
1311                                 isStatus = isUnarchived and isUnread
1312                         elif status == cls.UNARCHIVED_STATUS:
1313                                 isStatus = isUnarchived
1314                         else:
1315                                 assert "Status %s is bad for %r" % (status, message)
1316
1317                 return isType and isStatus
1318
1319         _MIN_MESSAGES_SHOWN = 4
1320
1321         def _idly_populate_messageview(self):
1322                 with gtk_toolbox.gtk_lock():
1323                         banner = hildonize.show_busy_banner_start(self._window, "Loading Messages")
1324                 try:
1325                         self._messagemodel.clear()
1326                         self._isPopulated = True
1327
1328                         if self._messageType == self.NO_MESSAGES:
1329                                 messageItems = []
1330                         else:
1331                                 try:
1332                                         messageItems = self._backend.get_messages()
1333                                 except Exception, e:
1334                                         self._errorDisplay.push_exception_with_lock()
1335                                         self._isPopulated = False
1336                                         messageItems = []
1337
1338                         messageItems = (
1339                                 (gv_backend.decorate_message(message), message)
1340                                 for message in gv_backend.sort_messages(messageItems)
1341                         )
1342
1343                         for (contactId, header, number, relativeDate, messages), messageData in messageItems:
1344                                 prettyNumber = number[2:] if number.startswith("+1") else number
1345                                 prettyNumber = make_pretty(prettyNumber)
1346
1347                                 firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1348                                 expandedMessages = [firstMessage]
1349                                 expandedMessages.extend(messages)
1350                                 if (self._MIN_MESSAGES_SHOWN + 1) < len(messages):
1351                                         firstMessage = "<b>%s - %s</b> <i>(%s)</i>" % (header, prettyNumber, relativeDate)
1352                                         secondMessage = "<i>%d Messages Hidden...</i>" % (len(messages) - self._MIN_MESSAGES_SHOWN, )
1353                                         collapsedMessages = [firstMessage, secondMessage]
1354                                         collapsedMessages.extend(messages[-(self._MIN_MESSAGES_SHOWN+0):])
1355                                 else:
1356                                         collapsedMessages = expandedMessages
1357                                 #collapsedMessages = _collapse_message(collapsedMessages, 60, self._MIN_MESSAGES_SHOWN)
1358
1359                                 number = make_ugly(number)
1360
1361                                 row = number, relativeDate, header, "\n".join(collapsedMessages), expandedMessages, contactId, messageData
1362                                 with gtk_toolbox.gtk_lock():
1363                                         self._messagemodel.append(row)
1364                 except Exception, e:
1365                         self._errorDisplay.push_exception_with_lock()
1366                 finally:
1367                         with gtk_toolbox.gtk_lock():
1368                                 hildonize.show_busy_banner_end(banner)
1369                                 self._messagemodelfiltered.refilter()
1370
1371                 return False
1372
1373         def _on_messageview_row_activated(self, treeview, path, view_column):
1374                 try:
1375                         childPath = self._messagemodelfiltered.convert_path_to_child_path(path)
1376                         itr = self._messagemodel.get_iter(childPath)
1377                         if not itr:
1378                                 return
1379
1380                         number = make_ugly(self._messagemodel.get_value(itr, self.NUMBER_IDX))
1381                         description = self._messagemodel.get_value(itr, self.MESSAGES_IDX)
1382
1383                         contactId = self._messagemodel.get_value(itr, self.FROM_ID_IDX)
1384                         if contactId:
1385                                 contactPhoneNumbers = list(self._backend.get_contact_details(contactId))
1386                                 defaultMatches = [
1387                                         (number == make_ugly(contactNumber) or number[1:] == make_ugly(contactNumber))
1388                                         for (numberDescription, contactNumber) in contactPhoneNumbers
1389                                 ]
1390                                 try:
1391                                         defaultIndex = defaultMatches.index(True)
1392                                 except ValueError:
1393                                         contactPhoneNumbers.append(("Other", number))
1394                                         defaultIndex = len(contactPhoneNumbers)-1
1395                                         _moduleLogger.warn(
1396                                                 "Could not find contact %r's number %s among %r" % (
1397                                                         contactId, number, contactPhoneNumbers
1398                                                 )
1399                                         )
1400                         else:
1401                                 contactPhoneNumbers = [("Phone", number)]
1402                                 defaultIndex = -1
1403
1404                         self.add_contact(
1405                                 contactPhoneNumbers,
1406                                 messages = description,
1407                                 parent = self._window,
1408                                 defaultIndex = defaultIndex,
1409                         )
1410                         self._messageviewselection.unselect_all()
1411                 except Exception, e:
1412                         self._errorDisplay.push_exception()
1413
1414         def _on_message_type_clicked(self, *args, **kwds):
1415                 try:
1416                         selectedIndex = self.MESSAGE_TYPES.index(self._messageType)
1417
1418                         try:
1419                                 newSelectedIndex = hildonize.touch_selector(
1420                                         self._window,
1421                                         "Message Type",
1422                                         self.MESSAGE_TYPES,
1423                                         selectedIndex,
1424                                 )
1425                         except RuntimeError:
1426                                 return
1427
1428                         if selectedIndex != newSelectedIndex:
1429                                 self._messageType = self.MESSAGE_TYPES[newSelectedIndex]
1430                                 self._messageTypeButton.set_label(self._messageType)
1431                                 self._messagemodelfiltered.refilter()
1432                 except Exception, e:
1433                         self._errorDisplay.push_exception()
1434
1435         def _on_message_status_clicked(self, *args, **kwds):
1436                 try:
1437                         selectedIndex = self.MESSAGE_STATUSES.index(self._messageStatus)
1438
1439                         try:
1440                                 newSelectedIndex = hildonize.touch_selector(
1441                                         self._window,
1442                                         "Message Status",
1443                                         self.MESSAGE_STATUSES,
1444                                         selectedIndex,
1445                                 )
1446                         except RuntimeError:
1447                                 return
1448
1449                         if selectedIndex != newSelectedIndex:
1450                                 self._messageStatus = self.MESSAGE_STATUSES[newSelectedIndex]
1451                                 self._messageStatusButton.set_label(self._messageStatus)
1452                                 self._messagemodelfiltered.refilter()
1453                 except Exception, e:
1454                         self._errorDisplay.push_exception()
1455
1456
1457 class ContactsView(object):
1458
1459         CONTACT_TYPE_IDX = 0
1460         CONTACT_NAME_IDX = 1
1461         CONTACT_ID_IDX = 2
1462
1463         def __init__(self, widgetTree, backend, errorDisplay):
1464                 self._errorDisplay = errorDisplay
1465                 self._backend = backend
1466
1467                 self._addressBook = None
1468                 self._selectedComboIndex = 0
1469                 self._addressBookFactories = [null_backend.NullAddressBook()]
1470
1471                 self._booksList = []
1472                 self._bookSelectionButton = widgetTree.get_widget("addressbookSelectButton")
1473
1474                 self._isPopulated = False
1475                 self._contactsmodel = gtk.ListStore(
1476                         gobject.TYPE_STRING, # Contact Type
1477                         gobject.TYPE_STRING, # Contact Name
1478                         gobject.TYPE_STRING, # Contact ID
1479                 )
1480                 self._contactsviewselection = None
1481                 self._contactsview = widgetTree.get_widget("contactsview")
1482
1483                 self._contactColumn = gtk.TreeViewColumn("Contact")
1484                 displayContactSource = False
1485                 if displayContactSource:
1486                         textrenderer = gtk.CellRendererText()
1487                         self._contactColumn.pack_start(textrenderer, expand=False)
1488                         self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_TYPE_IDX)
1489                 textrenderer = gtk.CellRendererText()
1490                 hildonize.set_cell_thumb_selectable(textrenderer)
1491                 self._contactColumn.pack_start(textrenderer, expand=True)
1492                 self._contactColumn.add_attribute(textrenderer, 'text', self.CONTACT_NAME_IDX)
1493                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1494                 self._contactColumn.set_sort_column_id(1)
1495                 self._contactColumn.set_visible(True)
1496
1497                 self._onContactsviewRowActivatedId = 0
1498                 self._onAddressbookButtonChangedId = 0
1499                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1500
1501                 self._updateSink = gtk_toolbox.threaded_stage(
1502                         gtk_toolbox.comap(
1503                                 self._idly_populate_contactsview,
1504                                 gtk_toolbox.null_sink(),
1505                         )
1506                 )
1507
1508         def enable(self):
1509                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1510
1511                 self._contactsview.set_model(self._contactsmodel)
1512                 self._contactsview.set_fixed_height_mode(False)
1513                 self._contactsview.append_column(self._contactColumn)
1514                 self._contactsviewselection = self._contactsview.get_selection()
1515                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1516
1517                 del self._booksList[:]
1518                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1519                         if factoryName and bookName:
1520                                 entryName = "%s: %s" % (factoryName, bookName)
1521                         elif factoryName:
1522                                 entryName = factoryName
1523                         elif bookName:
1524                                 entryName = bookName
1525                         else:
1526                                 entryName = "Bad name (%d)" % factoryId
1527                         row = (str(factoryId), bookId, entryName)
1528                         self._booksList.append(row)
1529
1530                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1531                 self._onAddressbookButtonChangedId = self._bookSelectionButton.connect("clicked", self._on_addressbook_button_changed)
1532
1533                 if len(self._booksList) <= self._selectedComboIndex:
1534                         self._selectedComboIndex = 0
1535                 self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1536
1537                 selectedFactoryId = self._booksList[self._selectedComboIndex][0]
1538                 selectedBookId = self._booksList[self._selectedComboIndex][1]
1539                 self.open_addressbook(selectedFactoryId, selectedBookId)
1540
1541         def disable(self):
1542                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1543                 self._bookSelectionButton.disconnect(self._onAddressbookButtonChangedId)
1544
1545                 self.clear()
1546
1547                 self._bookSelectionButton.set_label("")
1548                 self._contactsview.set_model(None)
1549                 self._contactsview.remove_column(self._contactColumn)
1550
1551         def add_contact(self, *args, **kwds):
1552                 """
1553                 @note Actual dial function is patched in later
1554                 """
1555                 raise NotImplementedError("Horrible unknown error has occurred")
1556
1557         def get_addressbooks(self):
1558                 """
1559                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1560                 """
1561                 for i, factory in enumerate(self._addressBookFactories):
1562                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1563                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1564
1565         def open_addressbook(self, bookFactoryId, bookId):
1566                 bookFactoryIndex = int(bookFactoryId)
1567                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1568                 self._addressBook = addressBook
1569
1570         def update(self, force = False):
1571                 if not force and self._isPopulated:
1572                         return False
1573                 self._updateSink.send(())
1574                 return True
1575
1576         def clear(self):
1577                 self._isPopulated = False
1578                 self._contactsmodel.clear()
1579                 for factory in self._addressBookFactories:
1580                         factory.clear_caches()
1581                 self._addressBook.clear_caches()
1582
1583         def append(self, book):
1584                 self._addressBookFactories.append(book)
1585
1586         def extend(self, books):
1587                 self._addressBookFactories.extend(books)
1588
1589         @staticmethod
1590         def name():
1591                 return "Contacts"
1592
1593         def load_settings(self, config, sectionName):
1594                 try:
1595                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1596                 except ConfigParser.NoOptionError:
1597                         self._selectedComboIndex = 0
1598
1599         def save_settings(self, config, sectionName):
1600                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1601
1602         def _idly_populate_contactsview(self):
1603                 with gtk_toolbox.gtk_lock():
1604                         banner = hildonize.show_busy_banner_start(self._window, "Loading Contacts")
1605                 try:
1606                         addressBook = None
1607                         while addressBook is not self._addressBook:
1608                                 addressBook = self._addressBook
1609                                 with gtk_toolbox.gtk_lock():
1610                                         self._contactsview.set_model(None)
1611                                         self.clear()
1612
1613                                 try:
1614                                         contacts = addressBook.get_contacts()
1615                                 except Exception, e:
1616                                         contacts = []
1617                                         self._isPopulated = False
1618                                         self._errorDisplay.push_exception_with_lock()
1619                                 for contactId, contactName in contacts:
1620                                         contactType = addressBook.contact_source_short_name(contactId)
1621                                         row = contactType, contactName, contactId
1622                                         self._contactsmodel.append(row)
1623
1624                                 with gtk_toolbox.gtk_lock():
1625                                         self._contactsview.set_model(self._contactsmodel)
1626
1627                         self._isPopulated = True
1628                 except Exception, e:
1629                         self._errorDisplay.push_exception_with_lock()
1630                 finally:
1631                         with gtk_toolbox.gtk_lock():
1632                                 hildonize.show_busy_banner_end(banner)
1633                 return False
1634
1635         def _on_addressbook_button_changed(self, *args, **kwds):
1636                 try:
1637                         try:
1638                                 newSelectedComboIndex = hildonize.touch_selector(
1639                                         self._window,
1640                                         "Addressbook",
1641                                         (("%s" % m[2]) for m in self._booksList),
1642                                         self._selectedComboIndex,
1643                                 )
1644                         except RuntimeError:
1645                                 return
1646
1647                         selectedFactoryId = self._booksList[newSelectedComboIndex][0]
1648                         selectedBookId = self._booksList[newSelectedComboIndex][1]
1649
1650                         oldAddressbook = self._addressBook
1651                         self.open_addressbook(selectedFactoryId, selectedBookId)
1652                         forceUpdate = True if oldAddressbook is not self._addressBook else False
1653                         self.update(force=forceUpdate)
1654
1655                         self._selectedComboIndex = newSelectedComboIndex
1656                         self._bookSelectionButton.set_label(self._booksList[self._selectedComboIndex][2])
1657                 except Exception, e:
1658                         self._errorDisplay.push_exception()
1659
1660         def _on_contactsview_row_activated(self, treeview, path, view_column):
1661                 try:
1662                         itr = self._contactsmodel.get_iter(path)
1663                         if not itr:
1664                                 return
1665
1666                         contactId = self._contactsmodel.get_value(itr, self.CONTACT_ID_IDX)
1667                         contactName = self._contactsmodel.get_value(itr, self.CONTACT_NAME_IDX)
1668                         try:
1669                                 contactDetails = self._addressBook.get_contact_details(contactId)
1670                         except Exception, e:
1671                                 contactDetails = []
1672                                 self._errorDisplay.push_exception()
1673                         contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1674
1675                         if len(contactPhoneNumbers) == 0:
1676                                 return
1677
1678                         self.add_contact(
1679                                 contactPhoneNumbers,
1680                                 messages = (contactName, ),
1681                                 parent = self._window,
1682                         )
1683                         self._contactsviewselection.unselect_all()
1684                 except Exception, e:
1685                         self._errorDisplay.push_exception()