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