Minor hildonization fix
[gc-dialer] / src / gv_views.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's Grand Central 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
22 from __future__ import with_statement
23
24 import ConfigParser
25 import warnings
26
27 import gobject
28 import pango
29 import gtk
30
31 import gtk_toolbox
32 import hildonize
33 import null_backend
34
35
36 def make_ugly(prettynumber):
37         """
38         function to take a phone number and strip out all non-numeric
39         characters
40
41         >>> make_ugly("+012-(345)-678-90")
42         '01234567890'
43         """
44         import re
45         uglynumber = re.sub('\D', '', prettynumber)
46         return uglynumber
47
48
49 def make_pretty(phonenumber):
50         """
51         Function to take a phone number and return the pretty version
52         pretty numbers:
53                 if phonenumber begins with 0:
54                         ...-(...)-...-....
55                 if phonenumber begins with 1: ( for gizmo callback numbers )
56                         1 (...)-...-....
57                 if phonenumber is 13 digits:
58                         (...)-...-....
59                 if phonenumber is 10 digits:
60                         ...-....
61         >>> make_pretty("12")
62         '12'
63         >>> make_pretty("1234567")
64         '123-4567'
65         >>> make_pretty("2345678901")
66         '(234)-567-8901'
67         >>> make_pretty("12345678901")
68         '1 (234)-567-8901'
69         >>> make_pretty("01234567890")
70         '+012-(345)-678-90'
71         """
72         if phonenumber is None or phonenumber is "":
73                 return ""
74
75         phonenumber = make_ugly(phonenumber)
76
77         if len(phonenumber) < 3:
78                 return phonenumber
79
80         if phonenumber[0] == "0":
81                 prettynumber = ""
82                 prettynumber += "+%s" % phonenumber[0:3]
83                 if 3 < len(phonenumber):
84                         prettynumber += "-(%s)" % phonenumber[3:6]
85                         if 6 < len(phonenumber):
86                                 prettynumber += "-%s" % phonenumber[6:9]
87                                 if 9 < len(phonenumber):
88                                         prettynumber += "-%s" % phonenumber[9:]
89                 return prettynumber
90         elif len(phonenumber) <= 7:
91                 prettynumber = "%s-%s" % (phonenumber[0:3], phonenumber[3:])
92         elif len(phonenumber) > 8 and phonenumber[0] == "1":
93                 prettynumber = "1 (%s)-%s-%s" % (phonenumber[1:4], phonenumber[4:7], phonenumber[7:])
94         elif len(phonenumber) > 7:
95                 prettynumber = "(%s)-%s-%s" % (phonenumber[0:3], phonenumber[3:6], phonenumber[6:])
96         return prettynumber
97
98
99 class MergedAddressBook(object):
100         """
101         Merger of all addressbooks
102         """
103
104         def __init__(self, addressbookFactories, sorter = None):
105                 self.__addressbookFactories = addressbookFactories
106                 self.__addressbooks = None
107                 self.__sort_contacts = sorter if sorter is not None else self.null_sorter
108
109         def clear_caches(self):
110                 self.__addressbooks = None
111                 for factory in self.__addressbookFactories:
112                         factory.clear_caches()
113
114         def get_addressbooks(self):
115                 """
116                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
117                 """
118                 yield self, "", ""
119
120         def open_addressbook(self, bookId):
121                 return self
122
123         def contact_source_short_name(self, contactId):
124                 if self.__addressbooks is None:
125                         return ""
126                 bookIndex, originalId = contactId.split("-", 1)
127                 return self.__addressbooks[int(bookIndex)].contact_source_short_name(originalId)
128
129         @staticmethod
130         def factory_name():
131                 return "All Contacts"
132
133         def get_contacts(self):
134                 """
135                 @returns Iterable of (contact id, contact name)
136                 """
137                 if self.__addressbooks is None:
138                         self.__addressbooks = list(
139                                 factory.open_addressbook(id)
140                                 for factory in self.__addressbookFactories
141                                 for (f, id, name) in factory.get_addressbooks()
142                         )
143                 contacts = (
144                         ("-".join([str(bookIndex), contactId]), contactName)
145                                 for (bookIndex, addressbook) in enumerate(self.__addressbooks)
146                                         for (contactId, contactName) in addressbook.get_contacts()
147                 )
148                 sortedContacts = self.__sort_contacts(contacts)
149                 return sortedContacts
150
151         def get_contact_details(self, contactId):
152                 """
153                 @returns Iterable of (Phone Type, Phone Number)
154                 """
155                 if self.__addressbooks is None:
156                         return []
157                 bookIndex, originalId = contactId.split("-", 1)
158                 return self.__addressbooks[int(bookIndex)].get_contact_details(originalId)
159
160         @staticmethod
161         def null_sorter(contacts):
162                 """
163                 Good for speed/low memory
164                 """
165                 return contacts
166
167         @staticmethod
168         def basic_firtname_sorter(contacts):
169                 """
170                 Expects names in "First Last" format
171                 """
172                 contactsWithKey = [
173                         (contactName.rsplit(" ", 1)[0], (contactId, contactName))
174                                 for (contactId, contactName) in contacts
175                 ]
176                 contactsWithKey.sort()
177                 return (contactData for (lastName, contactData) in contactsWithKey)
178
179         @staticmethod
180         def basic_lastname_sorter(contacts):
181                 """
182                 Expects names in "First Last" format
183                 """
184                 contactsWithKey = [
185                         (contactName.rsplit(" ", 1)[-1], (contactId, contactName))
186                                 for (contactId, contactName) in contacts
187                 ]
188                 contactsWithKey.sort()
189                 return (contactData for (lastName, contactData) in contactsWithKey)
190
191         @staticmethod
192         def reversed_firtname_sorter(contacts):
193                 """
194                 Expects names in "Last, First" format
195                 """
196                 contactsWithKey = [
197                         (contactName.split(", ", 1)[-1], (contactId, contactName))
198                                 for (contactId, contactName) in contacts
199                 ]
200                 contactsWithKey.sort()
201                 return (contactData for (lastName, contactData) in contactsWithKey)
202
203         @staticmethod
204         def reversed_lastname_sorter(contacts):
205                 """
206                 Expects names in "Last, First" format
207                 """
208                 contactsWithKey = [
209                         (contactName.split(", ", 1)[0], (contactId, contactName))
210                                 for (contactId, contactName) in contacts
211                 ]
212                 contactsWithKey.sort()
213                 return (contactData for (lastName, contactData) in contactsWithKey)
214
215         @staticmethod
216         def guess_firstname(name):
217                 if ", " in name:
218                         return name.split(", ", 1)[-1]
219                 else:
220                         return name.rsplit(" ", 1)[0]
221
222         @staticmethod
223         def guess_lastname(name):
224                 if ", " in name:
225                         return name.split(", ", 1)[0]
226                 else:
227                         return name.rsplit(" ", 1)[-1]
228
229         @classmethod
230         def advanced_firstname_sorter(cls, contacts):
231                 contactsWithKey = [
232                         (cls.guess_firstname(contactName), (contactId, contactName))
233                                 for (contactId, contactName) in contacts
234                 ]
235                 contactsWithKey.sort()
236                 return (contactData for (lastName, contactData) in contactsWithKey)
237
238         @classmethod
239         def advanced_lastname_sorter(cls, contacts):
240                 contactsWithKey = [
241                         (cls.guess_lastname(contactName), (contactId, contactName))
242                                 for (contactId, contactName) in contacts
243                 ]
244                 contactsWithKey.sort()
245                 return (contactData for (lastName, contactData) in contactsWithKey)
246
247
248 class PhoneTypeSelector(object):
249
250         ACTION_CANCEL = "cancel"
251         ACTION_SELECT = "select"
252         ACTION_DIAL = "dial"
253         ACTION_SEND_SMS = "sms"
254
255         def __init__(self, widgetTree, gcBackend):
256                 self._gcBackend = gcBackend
257                 self._widgetTree = widgetTree
258
259                 self._dialog = self._widgetTree.get_widget("phonetype_dialog")
260                 self._smsDialog = SmsEntryDialog(self._widgetTree)
261
262                 self._smsButton = self._widgetTree.get_widget("sms_button")
263                 self._smsButton.connect("clicked", self._on_phonetype_send_sms)
264
265                 self._dialButton = self._widgetTree.get_widget("dial_button")
266                 self._dialButton.connect("clicked", self._on_phonetype_dial)
267
268                 self._selectButton = self._widgetTree.get_widget("select_button")
269                 self._selectButton.connect("clicked", self._on_phonetype_select)
270
271                 self._cancelButton = self._widgetTree.get_widget("cancel_button")
272                 self._cancelButton.connect("clicked", self._on_phonetype_cancel)
273
274                 self._typemodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING)
275                 self._typeviewselection = None
276
277                 self._message = self._widgetTree.get_widget("phoneSelectionMessage")
278                 self._messageViewport = self._widgetTree.get_widget("phoneSelectionMessage_viewport")
279                 self._scrollWindow = self._widgetTree.get_widget("phoneSelectionMessage_scrolledwindow")
280                 self._typeview = self._widgetTree.get_widget("phonetypes")
281                 self._typeview.connect("row-activated", self._on_phonetype_select)
282
283                 self._action = self.ACTION_CANCEL
284
285         def run(self, contactDetails, message = "", parent = None):
286                 self._action = self.ACTION_CANCEL
287                 self._typemodel.clear()
288                 self._typeview.set_model(self._typemodel)
289
290                 # Add the column to the treeview
291                 textrenderer = gtk.CellRendererText()
292                 numberColumn = gtk.TreeViewColumn("Phone Numbers", textrenderer, text=0)
293                 self._typeview.append_column(numberColumn)
294
295                 textrenderer = gtk.CellRendererText()
296                 typeColumn = gtk.TreeViewColumn("Phone Type", textrenderer, text=1)
297                 self._typeview.append_column(typeColumn)
298
299                 self._typeviewselection = self._typeview.get_selection()
300                 self._typeviewselection.set_mode(gtk.SELECTION_SINGLE)
301
302                 for phoneType, phoneNumber in contactDetails:
303                         display = " - ".join((phoneNumber, phoneType))
304                         display = phoneType
305                         row = (phoneNumber, display)
306                         self._typemodel.append(row)
307
308                 self._typeviewselection.select_iter(self._typemodel.get_iter_first())
309                 if message:
310                         self._message.set_markup(message)
311                         self._message.show()
312                 else:
313                         self._message.set_markup("")
314                         self._message.hide()
315
316                 if parent is not None:
317                         self._dialog.set_transient_for(parent)
318
319                 try:
320                         self._dialog.show()
321                         adjustment = self._scrollWindow.get_vadjustment()
322                         dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
323                         dx = max(dx, 0)
324                         adjustment.value = dx
325
326                         userResponse = self._dialog.run()
327                 finally:
328                         self._dialog.hide()
329
330                 if userResponse == gtk.RESPONSE_OK:
331                         phoneNumber = self._get_number()
332                         phoneNumber = make_ugly(phoneNumber)
333                 else:
334                         phoneNumber = ""
335                 if not phoneNumber:
336                         self._action = self.ACTION_CANCEL
337
338                 if self._action == self.ACTION_SEND_SMS:
339                         smsMessage = self._smsDialog.run(phoneNumber, message, parent)
340                         if not smsMessage:
341                                 phoneNumber = ""
342                                 self._action = self.ACTION_CANCEL
343                 else:
344                         smsMessage = ""
345
346                 self._typeviewselection.unselect_all()
347                 self._typeview.remove_column(numberColumn)
348                 self._typeview.remove_column(typeColumn)
349                 self._typeview.set_model(None)
350
351                 return self._action, phoneNumber, smsMessage
352
353         def _get_number(self):
354                 model, itr = self._typeviewselection.get_selected()
355                 if not itr:
356                         return ""
357
358                 phoneNumber = self._typemodel.get_value(itr, 0)
359                 return phoneNumber
360
361         def _on_phonetype_dial(self, *args):
362                 self._dialog.response(gtk.RESPONSE_OK)
363                 self._action = self.ACTION_DIAL
364
365         def _on_phonetype_send_sms(self, *args):
366                 self._dialog.response(gtk.RESPONSE_OK)
367                 self._action = self.ACTION_SEND_SMS
368
369         def _on_phonetype_select(self, *args):
370                 self._dialog.response(gtk.RESPONSE_OK)
371                 self._action = self.ACTION_SELECT
372
373         def _on_phonetype_cancel(self, *args):
374                 self._dialog.response(gtk.RESPONSE_CANCEL)
375                 self._action = self.ACTION_CANCEL
376
377
378 class SmsEntryDialog(object):
379
380         """
381         @todo Add multi-SMS messages like GoogleVoice
382         """
383
384         MAX_CHAR = 160
385
386         def __init__(self, widgetTree):
387                 self._widgetTree = widgetTree
388                 self._dialog = self._widgetTree.get_widget("smsDialog")
389
390                 self._smsButton = self._widgetTree.get_widget("sendSmsButton")
391                 self._smsButton.connect("clicked", self._on_send)
392
393                 self._cancelButton = self._widgetTree.get_widget("cancelSmsButton")
394                 self._cancelButton.connect("clicked", self._on_cancel)
395
396                 self._letterCountLabel = self._widgetTree.get_widget("smsLetterCount")
397                 self._message = self._widgetTree.get_widget("smsMessage")
398                 self._messageViewport = self._widgetTree.get_widget("smsMessage_viewport")
399                 self._scrollWindow = self._widgetTree.get_widget("smsMessage_scrolledwindow")
400                 self._smsEntry = self._widgetTree.get_widget("smsEntry")
401                 self._smsEntry.get_buffer().connect("changed", self._on_entry_changed)
402
403         def run(self, number, message = "", parent = None):
404                 if message:
405                         self._message.set_markup(message)
406                         self._message.show()
407                 else:
408                         self._message.set_markup("")
409                         self._message.hide()
410                 self._smsEntry.get_buffer().set_text("")
411                 self._update_letter_count()
412
413                 if parent is not None:
414                         self._dialog.set_transient_for(parent)
415
416                 try:
417                         self._dialog.show()
418                         adjustment = self._scrollWindow.get_vadjustment()
419                         dx = self._message.get_allocation().height - self._messageViewport.get_allocation().height
420                         dx = max(dx, 0)
421                         adjustment.value = dx
422
423                         userResponse = self._dialog.run()
424                 finally:
425                         self._dialog.hide()
426
427                 if userResponse == gtk.RESPONSE_OK:
428                         entryBuffer = self._smsEntry.get_buffer()
429                         enteredMessage = entryBuffer.get_text(entryBuffer.get_start_iter(), entryBuffer.get_end_iter())
430                         enteredMessage = enteredMessage[0:self.MAX_CHAR]
431                 else:
432                         enteredMessage = ""
433
434                 return enteredMessage.strip()
435
436         def _update_letter_count(self, *args):
437                 entryLength = self._smsEntry.get_buffer().get_char_count()
438                 charsLeft = self.MAX_CHAR - entryLength
439                 self._letterCountLabel.set_text(str(charsLeft))
440                 if charsLeft < 0:
441                         self._smsButton.set_sensitive(False)
442                 else:
443                         self._smsButton.set_sensitive(True)
444
445         def _on_entry_changed(self, *args):
446                 self._update_letter_count()
447
448         def _on_send(self, *args):
449                 self._dialog.response(gtk.RESPONSE_OK)
450
451         def _on_cancel(self, *args):
452                 self._dialog.response(gtk.RESPONSE_CANCEL)
453
454
455 class Dialpad(object):
456
457         def __init__(self, widgetTree, errorDisplay):
458                 self._errorDisplay = errorDisplay
459                 self._smsDialog = SmsEntryDialog(widgetTree)
460
461                 self._numberdisplay = widgetTree.get_widget("numberdisplay")
462                 self._dialButton = widgetTree.get_widget("dial")
463                 self._backButton = widgetTree.get_widget("back")
464                 self._phonenumber = ""
465                 self._prettynumber = ""
466
467                 callbackMapping = {
468                         "on_dial_clicked": self._on_dial_clicked,
469                         "on_sms_clicked": self._on_sms_clicked,
470                         "on_digit_clicked": self._on_digit_clicked,
471                         "on_clear_number": self._on_clear_number,
472                 }
473                 widgetTree.signal_autoconnect(callbackMapping)
474
475                 self._originalLabel = self._backButton.get_label()
476                 self._backTapHandler = gtk_toolbox.TapOrHold(self._backButton)
477                 self._backTapHandler.on_tap = self._on_backspace
478                 self._backTapHandler.on_hold = self._on_clearall
479                 self._backTapHandler.on_holding = self._set_clear_button
480                 self._backTapHandler.on_cancel = self._reset_back_button
481
482                 self._window = gtk_toolbox.find_parent_window(self._numberdisplay)
483
484         def enable(self):
485                 self._dialButton.grab_focus()
486                 self._backTapHandler.enable()
487
488         def disable(self):
489                 self._reset_back_button()
490                 self._backTapHandler.disable()
491
492         def number_selected(self, action, number, message):
493                 """
494                 @note Actual dial function is patched in later
495                 """
496                 raise NotImplementedError("Horrible unknown error has occurred")
497
498         def get_number(self):
499                 return self._phonenumber
500
501         def set_number(self, number):
502                 """
503                 Set the number to dial
504                 """
505                 try:
506                         self._phonenumber = make_ugly(number)
507                         self._prettynumber = make_pretty(self._phonenumber)
508                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
509                 except TypeError, e:
510                         self._errorDisplay.push_exception()
511
512         def clear(self):
513                 self.set_number("")
514
515         @staticmethod
516         def name():
517                 return "Dialpad"
518
519         def load_settings(self, config, section):
520                 pass
521
522         def save_settings(self, config, section):
523                 """
524                 @note Thread Agnostic
525                 """
526                 pass
527
528         def _on_sms_clicked(self, widget):
529                 action = PhoneTypeSelector.ACTION_SEND_SMS
530                 phoneNumber = self.get_number()
531
532                 message = self._smsDialog.run(phoneNumber, "", self._window)
533                 if not message:
534                         phoneNumber = ""
535                         action = PhoneTypeSelector.ACTION_CANCEL
536
537                 if action == PhoneTypeSelector.ACTION_CANCEL:
538                         return
539                 self.number_selected(action, phoneNumber, message)
540
541         def _on_dial_clicked(self, widget):
542                 action = PhoneTypeSelector.ACTION_DIAL
543                 phoneNumber = self.get_number()
544                 message = ""
545                 self.number_selected(action, phoneNumber, message)
546
547         def _on_clear_number(self, *args):
548                 self.clear()
549
550         def _on_digit_clicked(self, widget):
551                 self.set_number(self._phonenumber + widget.get_name()[-1])
552
553         def _on_backspace(self, taps):
554                 self.set_number(self._phonenumber[:-taps])
555                 self._reset_back_button()
556
557         def _on_clearall(self, taps):
558                 self.clear()
559                 self._reset_back_button()
560                 return False
561
562         def _set_clear_button(self):
563                 self._backButton.set_label("gtk-clear")
564
565         def _reset_back_button(self):
566                 self._backButton.set_label(self._originalLabel)
567
568
569 class AccountInfo(object):
570
571         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
572                 self._errorDisplay = errorDisplay
573                 self._backend = backend
574                 self._isPopulated = False
575                 self._alarmHandler = alarmHandler
576                 self._notifyOnMissed = False
577                 self._notifyOnVoicemail = False
578                 self._notifyOnSms = False
579
580                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
581                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
582                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
583                 self._onCallbackentryChangedId = 0
584
585                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
586                 self._minutesEntry = widgetTree.get_widget("minutesEntry")
587                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
588                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
589                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
590                 self._onNotifyToggled = 0
591                 self._onMinutesChanged = 0
592                 self._onMissedToggled = 0
593                 self._onVoicemailToggled = 0
594                 self._onSmsToggled = 0
595                 self._applyAlarmTimeoutId = None
596
597                 self._defaultCallback = ""
598
599         def enable(self):
600                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
601
602                 self._accountViewNumberDisplay.set_use_markup(True)
603                 self.set_account_number("")
604
605                 self._callbackList.clear()
606                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
607
608                 if self._alarmHandler is not None:
609                         self._minutesEntry.set_range(1, 60)
610
611                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
612                         self._minutesEntry.set_value(self._alarmHandler.recurrence)
613                         self._missedCheckbox.set_active(self._notifyOnMissed)
614                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
615                         self._smsCheckbox.set_active(self._notifyOnSms)
616
617                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
618                         self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed)
619                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
620                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
621                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
622                 else:
623                         self._notifyCheckbox.set_sensitive(False)
624                         self._minutesEntry.set_sensitive(False)
625                         self._missedCheckbox.set_sensitive(False)
626                         self._voicemailCheckbox.set_sensitive(False)
627                         self._smsCheckbox.set_sensitive(False)
628
629                 self.update(force=True)
630
631         def disable(self):
632                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
633                 self._onCallbackentryChangedId = 0
634
635                 if self._alarmHandler is not None:
636                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
637                         self._minutesEntry.disconnect(self._onMinutesChanged)
638                         self._missedCheckbox.disconnect(self._onNotifyToggled)
639                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
640                         self._smsCheckbox.disconnect(self._onNotifyToggled)
641                         self._onNotifyToggled = 0
642                         self._onMinutesChanged = 0
643                         self._onMissedToggled = 0
644                         self._onVoicemailToggled = 0
645                         self._onSmsToggled = 0
646                 else:
647                         self._notifyCheckbox.set_sensitive(True)
648                         self._minutesEntry.set_sensitive(True)
649                         self._missedCheckbox.set_sensitive(True)
650                         self._voicemailCheckbox.set_sensitive(True)
651                         self._smsCheckbox.set_sensitive(True)
652
653                 self.clear()
654                 self._callbackList.clear()
655
656         def get_selected_callback_number(self):
657                 return make_ugly(self._callbackCombo.get_child().get_text())
658
659         def set_account_number(self, number):
660                 """
661                 Displays current account number
662                 """
663                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
664
665         def update(self, force = False):
666                 if not force and self._isPopulated:
667                         return False
668                 self._populate_callback_combo()
669                 self.set_account_number(self._backend.get_account_number())
670                 return True
671
672         def clear(self):
673                 self._callbackCombo.get_child().set_text("")
674                 self.set_account_number("")
675                 self._isPopulated = False
676
677         def save_everything(self):
678                 raise NotImplementedError
679
680         @staticmethod
681         def name():
682                 return "Account Info"
683
684         def load_settings(self, config, section):
685                 self._defaultCallback = config.get(section, "callback")
686                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
687                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
688                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
689
690         def save_settings(self, config, section):
691                 """
692                 @note Thread Agnostic
693                 """
694                 callback = self.get_selected_callback_number()
695                 config.set(section, "callback", callback)
696                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
697                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
698                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
699
700         def _populate_callback_combo(self):
701                 self._isPopulated = True
702                 self._callbackList.clear()
703                 try:
704                         callbackNumbers = self._backend.get_callback_numbers()
705                 except StandardError, e:
706                         self._errorDisplay.push_exception()
707                         self._isPopulated = False
708                         return
709
710                 for number, description in callbackNumbers.iteritems():
711                         self._callbackList.append((make_pretty(number),))
712
713                 self._callbackCombo.set_model(self._callbackList)
714                 self._callbackCombo.set_text_column(0)
715                 #callbackNumber = self._backend.get_callback_number()
716                 callbackNumber = self._defaultCallback
717                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
718
719         def _set_callback_number(self, number):
720                 try:
721                         if not self._backend.is_valid_syntax(number):
722                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
723                         elif number == self._backend.get_callback_number():
724                                 warnings.warn(
725                                         "Callback number already is %s" % (
726                                                 self._backend.get_callback_number(),
727                                         ),
728                                         UserWarning,
729                                         2
730                                 )
731                         else:
732                                 self._backend.set_callback_number(number)
733                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
734                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
735                                 )
736                                 warnings.warn(
737                                         "Callback number set to %s" % (
738                                                 self._backend.get_callback_number(),
739                                         ),
740                                         UserWarning, 2
741                                 )
742                 except StandardError, e:
743                         self._errorDisplay.push_exception()
744
745         def _update_alarm_settings(self):
746                 try:
747                         isEnabled = self._notifyCheckbox.get_active()
748                         recurrence = self._minutesEntry.get_value_as_int()
749                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
750                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
751                 finally:
752                         self.save_everything()
753                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
754                         self._minutesEntry.set_value(self._alarmHandler.recurrence)
755
756         def _on_callbackentry_changed(self, *args):
757                 text = self.get_selected_callback_number()
758                 number = make_ugly(text)
759                 self._set_callback_number(number)
760
761         def _on_notify_toggled(self, *args):
762                 if self._applyAlarmTimeoutId is not None:
763                         gobject.source_remove(self._applyAlarmTimeoutId)
764                         self._applyAlarmTimeoutId = None
765                 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
766
767         def _on_minutes_changed(self, *args):
768                 if self._applyAlarmTimeoutId is not None:
769                         gobject.source_remove(self._applyAlarmTimeoutId)
770                         self._applyAlarmTimeoutId = None
771                 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
772
773         def _on_apply_timeout(self, *args):
774                 self._applyAlarmTimeoutId = None
775
776                 self._update_alarm_settings()
777                 return False
778
779         def _on_missed_toggled(self, *args):
780                 self._notifyOnMissed = self._missedCheckbox.get_active()
781                 self.save_everything()
782
783         def _on_voicemail_toggled(self, *args):
784                 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
785                 self.save_everything()
786
787         def _on_sms_toggled(self, *args):
788                 self._notifyOnSms = self._smsCheckbox.get_active()
789                 self.save_everything()
790
791
792 class RecentCallsView(object):
793
794         NUMBER_IDX = 0
795         DATE_IDX = 1
796         ACTION_IDX = 2
797         FROM_IDX = 3
798
799         def __init__(self, widgetTree, backend, errorDisplay):
800                 self._errorDisplay = errorDisplay
801                 self._backend = backend
802
803                 self._isPopulated = False
804                 self._recentmodel = gtk.ListStore(
805                         gobject.TYPE_STRING, # number
806                         gobject.TYPE_STRING, # date
807                         gobject.TYPE_STRING, # action
808                         gobject.TYPE_STRING, # from
809                 )
810                 self._recentview = widgetTree.get_widget("recentview")
811                 self._recentviewselection = None
812                 self._onRecentviewRowActivatedId = 0
813
814                 textrenderer = gtk.CellRendererText()
815                 textrenderer.set_property("yalign", 0)
816                 hildonize.set_cell_thumb_selectable(textrenderer)
817                 self._dateColumn = gtk.TreeViewColumn("Date")
818                 self._dateColumn.pack_start(textrenderer, expand=True)
819                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
820
821                 textrenderer = gtk.CellRendererText()
822                 textrenderer.set_property("yalign", 0)
823                 hildonize.set_cell_thumb_selectable(textrenderer)
824                 self._actionColumn = gtk.TreeViewColumn("Action")
825                 self._actionColumn.pack_start(textrenderer, expand=True)
826                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
827
828                 textrenderer = gtk.CellRendererText()
829                 textrenderer.set_property("yalign", 0)
830                 hildonize.set_cell_thumb_selectable(textrenderer)
831                 self._nameColumn = gtk.TreeViewColumn("From")
832                 self._nameColumn.pack_start(textrenderer, expand=True)
833                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
834                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
835
836                 textrenderer = gtk.CellRendererText()
837                 textrenderer.set_property("yalign", 0)
838                 hildonize.set_cell_thumb_selectable(textrenderer)
839                 self._numberColumn = gtk.TreeViewColumn("Number")
840                 self._numberColumn.pack_start(textrenderer, expand=True)
841                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
842
843                 self._window = gtk_toolbox.find_parent_window(self._recentview)
844                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
845
846                 self._updateSink = gtk_toolbox.threaded_stage(
847                         gtk_toolbox.comap(
848                                 self._idly_populate_recentview,
849                                 gtk_toolbox.null_sink(),
850                         )
851                 )
852
853         def enable(self):
854                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
855                 self._recentview.set_model(self._recentmodel)
856
857                 self._recentview.append_column(self._dateColumn)
858                 self._recentview.append_column(self._actionColumn)
859                 self._recentview.append_column(self._numberColumn)
860                 self._recentview.append_column(self._nameColumn)
861                 self._recentviewselection = self._recentview.get_selection()
862                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
863
864                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
865
866         def disable(self):
867                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
868
869                 self.clear()
870
871                 self._recentview.remove_column(self._dateColumn)
872                 self._recentview.remove_column(self._actionColumn)
873                 self._recentview.remove_column(self._nameColumn)
874                 self._recentview.remove_column(self._numberColumn)
875                 self._recentview.set_model(None)
876
877         def number_selected(self, action, number, message):
878                 """
879                 @note Actual dial function is patched in later
880                 """
881                 raise NotImplementedError("Horrible unknown error has occurred")
882
883         def update(self, force = False):
884                 if not force and self._isPopulated:
885                         return False
886                 self._updateSink.send(())
887                 return True
888
889         def clear(self):
890                 self._isPopulated = False
891                 self._recentmodel.clear()
892
893         @staticmethod
894         def name():
895                 return "Recent Calls"
896
897         def load_settings(self, config, section):
898                 pass
899
900         def save_settings(self, config, section):
901                 """
902                 @note Thread Agnostic
903                 """
904                 pass
905
906         def _idly_populate_recentview(self):
907                 self._recentmodel.clear()
908                 self._isPopulated = True
909
910                 try:
911                         recentItems = self._backend.get_recent()
912                 except StandardError, e:
913                         self._errorDisplay.push_exception_with_lock()
914                         self._isPopulated = False
915                         recentItems = []
916
917                 for personName, phoneNumber, date, action in recentItems:
918                         if not personName:
919                                 personName = "Unknown"
920                         prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
921                         prettyNumber = make_pretty(prettyNumber)
922                         item = (prettyNumber, date, action.capitalize(), personName)
923                         with gtk_toolbox.gtk_lock():
924                                 self._recentmodel.append(item)
925
926                 return False
927
928         def _on_recentview_row_activated(self, treeview, path, view_column):
929                 model, itr = self._recentviewselection.get_selected()
930                 if not itr:
931                         return
932
933                 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
934                 number = make_ugly(number)
935                 contactPhoneNumbers = [("Phone", number)]
936                 description = self._recentmodel.get_value(itr, self.FROM_IDX)
937
938                 action, phoneNumber, message = self._phoneTypeSelector.run(
939                         contactPhoneNumbers,
940                         message = description,
941                         parent = self._window,
942                 )
943                 if action == PhoneTypeSelector.ACTION_CANCEL:
944                         return
945                 assert phoneNumber, "A lack of phone number exists"
946
947                 self.number_selected(action, phoneNumber, message)
948                 self._recentviewselection.unselect_all()
949
950
951 class MessagesView(object):
952
953         NUMBER_IDX = 0
954         DATE_IDX = 1
955         HEADER_IDX = 2
956         MESSAGE_IDX = 3
957
958         def __init__(self, widgetTree, backend, errorDisplay):
959                 self._errorDisplay = errorDisplay
960                 self._backend = backend
961
962                 self._isPopulated = False
963                 self._messagemodel = gtk.ListStore(
964                         gobject.TYPE_STRING, # number
965                         gobject.TYPE_STRING, # date
966                         gobject.TYPE_STRING, # header
967                         gobject.TYPE_STRING, # message
968                 )
969                 self._messageview = widgetTree.get_widget("messages_view")
970                 self._messageviewselection = None
971                 self._onMessageviewRowActivatedId = 0
972
973                 self._messageRenderer = gtk.CellRendererText()
974                 hildonize.set_cell_thumb_selectable(self._messageRenderer)
975                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
976                 self._messageRenderer.set_property("wrap-width", 500)
977                 self._messageColumn = gtk.TreeViewColumn("Messages")
978                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
979                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
980                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
981
982                 self._window = gtk_toolbox.find_parent_window(self._messageview)
983                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
984
985                 self._updateSink = gtk_toolbox.threaded_stage(
986                         gtk_toolbox.comap(
987                                 self._idly_populate_messageview,
988                                 gtk_toolbox.null_sink(),
989                         )
990                 )
991
992         def enable(self):
993                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
994                 self._messageview.set_model(self._messagemodel)
995
996                 self._messageview.append_column(self._messageColumn)
997                 self._messageviewselection = self._messageview.get_selection()
998                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
999
1000                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1001
1002         def disable(self):
1003                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1004
1005                 self.clear()
1006
1007                 self._messageview.remove_column(self._messageColumn)
1008                 self._messageview.set_model(None)
1009
1010         def number_selected(self, action, number, message):
1011                 """
1012                 @note Actual dial function is patched in later
1013                 """
1014                 raise NotImplementedError("Horrible unknown error has occurred")
1015
1016         def update(self, force = False):
1017                 if not force and self._isPopulated:
1018                         return False
1019                 self._updateSink.send(())
1020                 return True
1021
1022         def clear(self):
1023                 self._isPopulated = False
1024                 self._messagemodel.clear()
1025
1026         @staticmethod
1027         def name():
1028                 return "Messages"
1029
1030         def load_settings(self, config, section):
1031                 pass
1032
1033         def save_settings(self, config, section):
1034                 """
1035                 @note Thread Agnostic
1036                 """
1037                 pass
1038
1039         def _idly_populate_messageview(self):
1040                 self._messagemodel.clear()
1041                 self._isPopulated = True
1042
1043                 try:
1044                         messageItems = self._backend.get_messages()
1045                 except StandardError, e:
1046                         self._errorDisplay.push_exception_with_lock()
1047                         self._isPopulated = False
1048                         messageItems = []
1049
1050                 for header, number, relativeDate, message in messageItems:
1051                         prettyNumber = number[2:] if number.startswith("+1") else number
1052                         prettyNumber = make_pretty(prettyNumber)
1053                         message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1054                         number = make_ugly(number)
1055                         row = (number, relativeDate, header, message)
1056                         with gtk_toolbox.gtk_lock():
1057                                 self._messagemodel.append(row)
1058
1059                 return False
1060
1061         def _on_messageview_row_activated(self, treeview, path, view_column):
1062                 model, itr = self._messageviewselection.get_selected()
1063                 if not itr:
1064                         return
1065
1066                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1067                 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1068
1069                 action, phoneNumber, message = self._phoneTypeSelector.run(
1070                         contactPhoneNumbers,
1071                         message = description,
1072                         parent = self._window,
1073                 )
1074                 if action == PhoneTypeSelector.ACTION_CANCEL:
1075                         return
1076                 assert phoneNumber, "A lock of phone number exists"
1077
1078                 self.number_selected(action, phoneNumber, message)
1079                 self._messageviewselection.unselect_all()
1080
1081
1082 class ContactsView(object):
1083
1084         def __init__(self, widgetTree, backend, errorDisplay):
1085                 self._errorDisplay = errorDisplay
1086                 self._backend = backend
1087
1088                 self._addressBook = None
1089                 self._selectedComboIndex = 0
1090                 self._addressBookFactories = [null_backend.NullAddressBook()]
1091
1092                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1093                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1094
1095                 self._isPopulated = False
1096                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1097                 self._contactsviewselection = None
1098                 self._contactsview = widgetTree.get_widget("contactsview")
1099
1100                 self._contactColumn = gtk.TreeViewColumn("Contact")
1101                 displayContactSource = False
1102                 if displayContactSource:
1103                         textrenderer = gtk.CellRendererText()
1104                         self._contactColumn.pack_start(textrenderer, expand=False)
1105                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1106                 textrenderer = gtk.CellRendererText()
1107                 hildonize.set_cell_thumb_selectable(textrenderer)
1108                 self._contactColumn.pack_start(textrenderer, expand=True)
1109                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1110                 textrenderer = gtk.CellRendererText()
1111                 self._contactColumn.pack_start(textrenderer, expand=True)
1112                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1113                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1114                 self._contactColumn.set_sort_column_id(1)
1115                 self._contactColumn.set_visible(True)
1116
1117                 self._onContactsviewRowActivatedId = 0
1118                 self._onAddressbookComboChangedId = 0
1119                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1120                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1121
1122                 self._updateSink = gtk_toolbox.threaded_stage(
1123                         gtk_toolbox.comap(
1124                                 self._idly_populate_contactsview,
1125                                 gtk_toolbox.null_sink(),
1126                         )
1127                 )
1128
1129         def enable(self):
1130                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1131
1132                 self._contactsview.set_model(self._contactsmodel)
1133                 self._contactsview.append_column(self._contactColumn)
1134                 self._contactsviewselection = self._contactsview.get_selection()
1135                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1136
1137                 self._booksList.clear()
1138                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1139                         if factoryName and bookName:
1140                                 entryName = "%s: %s" % (factoryName, bookName)
1141                         elif factoryName:
1142                                 entryName = factoryName
1143                         elif bookName:
1144                                 entryName = bookName
1145                         else:
1146                                 entryName = "Bad name (%d)" % factoryId
1147                         row = (str(factoryId), bookId, entryName)
1148                         self._booksList.append(row)
1149
1150                 self._booksSelectionBox.set_model(self._booksList)
1151                 cell = gtk.CellRendererText()
1152                 self._booksSelectionBox.pack_start(cell, True)
1153                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1154
1155                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1156                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1157
1158                 if len(self._booksList) <= self._selectedComboIndex:
1159                         self._selectedComboIndex = 0
1160                 self._booksSelectionBox.set_active(self._selectedComboIndex)
1161
1162         def disable(self):
1163                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1164                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1165
1166                 self.clear()
1167
1168                 self._booksSelectionBox.clear()
1169                 self._booksSelectionBox.set_model(None)
1170                 self._contactsview.set_model(None)
1171                 self._contactsview.remove_column(self._contactColumn)
1172
1173         def number_selected(self, action, number, message):
1174                 """
1175                 @note Actual dial function is patched in later
1176                 """
1177                 raise NotImplementedError("Horrible unknown error has occurred")
1178
1179         def get_addressbooks(self):
1180                 """
1181                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1182                 """
1183                 for i, factory in enumerate(self._addressBookFactories):
1184                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1185                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1186
1187         def open_addressbook(self, bookFactoryId, bookId):
1188                 bookFactoryIndex = int(bookFactoryId)
1189                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1190
1191                 forceUpdate = True if addressBook is not self._addressBook else False
1192
1193                 self._addressBook = addressBook
1194                 self.update(force=forceUpdate)
1195
1196         def update(self, force = False):
1197                 if not force and self._isPopulated:
1198                         return False
1199                 self._updateSink.send(())
1200                 return True
1201
1202         def clear(self):
1203                 self._isPopulated = False
1204                 self._contactsmodel.clear()
1205                 for factory in self._addressBookFactories:
1206                         factory.clear_caches()
1207                 self._addressBook.clear_caches()
1208
1209         def append(self, book):
1210                 self._addressBookFactories.append(book)
1211
1212         def extend(self, books):
1213                 self._addressBookFactories.extend(books)
1214
1215         @staticmethod
1216         def name():
1217                 return "Contacts"
1218
1219         def load_settings(self, config, sectionName):
1220                 try:
1221                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1222                 except ConfigParser.NoOptionError:
1223                         self._selectedComboIndex = 0
1224
1225         def save_settings(self, config, sectionName):
1226                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1227
1228         def _idly_populate_contactsview(self):
1229                 addressBook = None
1230                 while addressBook is not self._addressBook:
1231                         addressBook = self._addressBook
1232                         with gtk_toolbox.gtk_lock():
1233                                 self._contactsview.set_model(None)
1234                                 self.clear()
1235
1236                         try:
1237                                 contacts = addressBook.get_contacts()
1238                         except StandardError, e:
1239                                 contacts = []
1240                                 self._isPopulated = False
1241                                 self._errorDisplay.push_exception_with_lock()
1242                         for contactId, contactName in contacts:
1243                                 contactType = (addressBook.contact_source_short_name(contactId), )
1244                                 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1245
1246                         with gtk_toolbox.gtk_lock():
1247                                 self._contactsview.set_model(self._contactsmodel)
1248
1249                 self._isPopulated = True
1250                 return False
1251
1252         def _on_addressbook_combo_changed(self, *args, **kwds):
1253                 itr = self._booksSelectionBox.get_active_iter()
1254                 if itr is None:
1255                         return
1256                 self._selectedComboIndex = self._booksSelectionBox.get_active()
1257                 selectedFactoryId = self._booksList.get_value(itr, 0)
1258                 selectedBookId = self._booksList.get_value(itr, 1)
1259                 self.open_addressbook(selectedFactoryId, selectedBookId)
1260
1261         def _on_contactsview_row_activated(self, treeview, path, view_column):
1262                 model, itr = self._contactsviewselection.get_selected()
1263                 if not itr:
1264                         return
1265
1266                 contactId = self._contactsmodel.get_value(itr, 3)
1267                 contactName = self._contactsmodel.get_value(itr, 1)
1268                 try:
1269                         contactDetails = self._addressBook.get_contact_details(contactId)
1270                 except StandardError, e:
1271                         contactDetails = []
1272                         self._errorDisplay.push_exception()
1273                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1274
1275                 if len(contactPhoneNumbers) == 0:
1276                         return
1277
1278                 action, phoneNumber, message = self._phoneTypeSelector.run(
1279                         contactPhoneNumbers,
1280                         message = contactName,
1281                         parent = self._window,
1282                 )
1283                 if action == PhoneTypeSelector.ACTION_CANCEL:
1284                         return
1285                 assert phoneNumber, "A lack of phone number exists"
1286
1287                 self.number_selected(action, phoneNumber, message)
1288                 self._contactsviewselection.unselect_all()