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