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