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