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