Rough removal of GrandCentral support
[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.set_number(self._phonenumber)
486                 self._dialButton.grab_focus()
487                 self._backTapHandler.enable()
488
489         def disable(self):
490                 self._reset_back_button()
491                 self._backTapHandler.disable()
492                 self.set_number("")
493
494         def number_selected(self, action, number, message):
495                 """
496                 @note Actual dial function is patched in later
497                 """
498                 raise NotImplementedError("Horrible unknown error has occurred")
499
500         def get_number(self):
501                 return self._phonenumber
502
503         def set_number(self, number):
504                 """
505                 Set the number to dial
506                 """
507                 try:
508                         self._phonenumber = make_ugly(number)
509                         self._prettynumber = make_pretty(self._phonenumber)
510                         self._numberdisplay.set_label("<span size='30000' weight='bold'>%s</span>" % (self._prettynumber))
511                 except TypeError, e:
512                         self._errorDisplay.push_exception()
513
514         def clear(self):
515                 self.set_number("")
516
517         @staticmethod
518         def name():
519                 return "Dialpad"
520
521         def load_settings(self, config, section):
522                 pass
523
524         def save_settings(self, config, section):
525                 """
526                 @note Thread Agnostic
527                 """
528                 pass
529
530         def _on_sms_clicked(self, widget):
531                 action = PhoneTypeSelector.ACTION_SEND_SMS
532                 phoneNumber = self.get_number()
533
534                 message = self._smsDialog.run(phoneNumber, "", self._window)
535                 if not message:
536                         phoneNumber = ""
537                         action = PhoneTypeSelector.ACTION_CANCEL
538
539                 if action == PhoneTypeSelector.ACTION_CANCEL:
540                         return
541                 self.number_selected(action, phoneNumber, message)
542
543         def _on_dial_clicked(self, widget):
544                 action = PhoneTypeSelector.ACTION_DIAL
545                 phoneNumber = self.get_number()
546                 message = ""
547                 self.number_selected(action, phoneNumber, message)
548
549         def _on_clear_number(self, *args):
550                 self.clear()
551
552         def _on_digit_clicked(self, widget):
553                 self.set_number(self._phonenumber + widget.get_name()[-1])
554
555         def _on_backspace(self, taps):
556                 self.set_number(self._phonenumber[:-taps])
557                 self._reset_back_button()
558
559         def _on_clearall(self, taps):
560                 self.clear()
561                 self._reset_back_button()
562                 return False
563
564         def _set_clear_button(self):
565                 self._backButton.set_label("gtk-clear")
566
567         def _reset_back_button(self):
568                 self._backButton.set_label(self._originalLabel)
569
570
571 class AccountInfo(object):
572
573         def __init__(self, widgetTree, backend, alarmHandler, errorDisplay):
574                 self._errorDisplay = errorDisplay
575                 self._backend = backend
576                 self._isPopulated = False
577                 self._alarmHandler = alarmHandler
578                 self._notifyOnMissed = False
579                 self._notifyOnVoicemail = False
580                 self._notifyOnSms = False
581
582                 self._callbackList = gtk.ListStore(gobject.TYPE_STRING)
583                 self._accountViewNumberDisplay = widgetTree.get_widget("gcnumber_display")
584                 self._callbackCombo = widgetTree.get_widget("callbackcombo")
585                 self._onCallbackentryChangedId = 0
586
587                 self._notifyCheckbox = widgetTree.get_widget("notifyCheckbox")
588                 self._minutesEntry = widgetTree.get_widget("minutesEntry")
589                 self._missedCheckbox = widgetTree.get_widget("missedCheckbox")
590                 self._voicemailCheckbox = widgetTree.get_widget("voicemailCheckbox")
591                 self._smsCheckbox = widgetTree.get_widget("smsCheckbox")
592                 self._onNotifyToggled = 0
593                 self._onMinutesChanged = 0
594                 self._onMissedToggled = 0
595                 self._onVoicemailToggled = 0
596                 self._onSmsToggled = 0
597                 self._applyAlarmTimeoutId = None
598
599                 self._defaultCallback = ""
600
601         def enable(self):
602                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
603
604                 self._accountViewNumberDisplay.set_use_markup(True)
605                 self.set_account_number("")
606
607                 self._callbackList.clear()
608                 self._onCallbackentryChangedId = self._callbackCombo.get_child().connect("changed", self._on_callbackentry_changed)
609
610                 if self._alarmHandler is not None:
611                         self._minutesEntry.set_range(1, 60)
612
613                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
614                         self._minutesEntry.set_value(self._alarmHandler.recurrence)
615                         self._missedCheckbox.set_active(self._notifyOnMissed)
616                         self._voicemailCheckbox.set_active(self._notifyOnVoicemail)
617                         self._smsCheckbox.set_active(self._notifyOnSms)
618
619                         self._onNotifyToggled = self._notifyCheckbox.connect("toggled", self._on_notify_toggled)
620                         self._onMinutesChanged = self._minutesEntry.connect("value-changed", self._on_minutes_changed)
621                         self._onMissedToggled = self._missedCheckbox.connect("toggled", self._on_missed_toggled)
622                         self._onVoicemailToggled = self._voicemailCheckbox.connect("toggled", self._on_voicemail_toggled)
623                         self._onSmsToggled = self._smsCheckbox.connect("toggled", self._on_sms_toggled)
624                 else:
625                         self._notifyCheckbox.set_sensitive(False)
626                         self._minutesEntry.set_sensitive(False)
627                         self._missedCheckbox.set_sensitive(False)
628                         self._voicemailCheckbox.set_sensitive(False)
629                         self._smsCheckbox.set_sensitive(False)
630
631                 self.update(force=True)
632
633         def disable(self):
634                 self._callbackCombo.get_child().disconnect(self._onCallbackentryChangedId)
635                 self._onCallbackentryChangedId = 0
636
637                 if self._alarmHandler is not None:
638                         self._notifyCheckbox.disconnect(self._onNotifyToggled)
639                         self._minutesEntry.disconnect(self._onMinutesChanged)
640                         self._missedCheckbox.disconnect(self._onNotifyToggled)
641                         self._voicemailCheckbox.disconnect(self._onNotifyToggled)
642                         self._smsCheckbox.disconnect(self._onNotifyToggled)
643                         self._onNotifyToggled = 0
644                         self._onMinutesChanged = 0
645                         self._onMissedToggled = 0
646                         self._onVoicemailToggled = 0
647                         self._onSmsToggled = 0
648                 else:
649                         self._notifyCheckbox.set_sensitive(True)
650                         self._minutesEntry.set_sensitive(True)
651                         self._missedCheckbox.set_sensitive(True)
652                         self._voicemailCheckbox.set_sensitive(True)
653                         self._smsCheckbox.set_sensitive(True)
654
655                 self.clear()
656                 self._callbackList.clear()
657
658         def get_selected_callback_number(self):
659                 return make_ugly(self._callbackCombo.get_child().get_text())
660
661         def set_account_number(self, number):
662                 """
663                 Displays current account number
664                 """
665                 self._accountViewNumberDisplay.set_label("<span size='23000' weight='bold'>%s</span>" % (number))
666
667         def update(self, force = False):
668                 if not force and self._isPopulated:
669                         return False
670                 self._populate_callback_combo()
671                 self.set_account_number(self._backend.get_account_number())
672                 return True
673
674         def clear(self):
675                 self._callbackCombo.get_child().set_text("")
676                 self.set_account_number("")
677                 self._isPopulated = False
678
679         def save_everything(self):
680                 raise NotImplementedError
681
682         @staticmethod
683         def name():
684                 return "Account Info"
685
686         def load_settings(self, config, section):
687                 self._defaultCallback = config.get(section, "callback")
688                 self._notifyOnMissed = config.getboolean(section, "notifyOnMissed")
689                 self._notifyOnVoicemail = config.getboolean(section, "notifyOnVoicemail")
690                 self._notifyOnSms = config.getboolean(section, "notifyOnSms")
691
692         def save_settings(self, config, section):
693                 """
694                 @note Thread Agnostic
695                 """
696                 callback = self.get_selected_callback_number()
697                 config.set(section, "callback", callback)
698                 config.set(section, "notifyOnMissed", repr(self._notifyOnMissed))
699                 config.set(section, "notifyOnVoicemail", repr(self._notifyOnVoicemail))
700                 config.set(section, "notifyOnSms", repr(self._notifyOnSms))
701
702         def _populate_callback_combo(self):
703                 self._isPopulated = True
704                 self._callbackList.clear()
705                 try:
706                         callbackNumbers = self._backend.get_callback_numbers()
707                 except StandardError, e:
708                         self._errorDisplay.push_exception()
709                         self._isPopulated = False
710                         return
711
712                 for number, description in callbackNumbers.iteritems():
713                         self._callbackList.append((make_pretty(number),))
714
715                 self._callbackCombo.set_model(self._callbackList)
716                 self._callbackCombo.set_text_column(0)
717                 #callbackNumber = self._backend.get_callback_number()
718                 callbackNumber = self._defaultCallback
719                 self._callbackCombo.get_child().set_text(make_pretty(callbackNumber))
720
721         def _set_callback_number(self, number):
722                 try:
723                         if not self._backend.is_valid_syntax(number):
724                                 self._errorDisplay.push_message("%s is not a valid callback number" % number)
725                         elif number == self._backend.get_callback_number():
726                                 warnings.warn(
727                                         "Callback number already is %s" % (
728                                                 self._backend.get_callback_number(),
729                                         ),
730                                         UserWarning,
731                                         2
732                                 )
733                         else:
734                                 self._backend.set_callback_number(number)
735                                 assert make_ugly(number) == make_ugly(self._backend.get_callback_number()), "Callback number should be %s but instead is %s" % (
736                                         make_pretty(number), make_pretty(self._backend.get_callback_number())
737                                 )
738                                 warnings.warn(
739                                         "Callback number set to %s" % (
740                                                 self._backend.get_callback_number(),
741                                         ),
742                                         UserWarning, 2
743                                 )
744                 except StandardError, e:
745                         self._errorDisplay.push_exception()
746
747         def _update_alarm_settings(self):
748                 try:
749                         isEnabled = self._notifyCheckbox.get_active()
750                         recurrence = self._minutesEntry.get_value_as_int()
751                         if isEnabled != self._alarmHandler.isEnabled or recurrence != self._alarmHandler.recurrence:
752                                 self._alarmHandler.apply_settings(isEnabled, recurrence)
753                 finally:
754                         self.save_everything()
755                         self._notifyCheckbox.set_active(self._alarmHandler.isEnabled)
756                         self._minutesEntry.set_value(self._alarmHandler.recurrence)
757
758         def _on_callbackentry_changed(self, *args):
759                 text = self.get_selected_callback_number()
760                 number = make_ugly(text)
761                 self._set_callback_number(number)
762
763         def _on_notify_toggled(self, *args):
764                 if self._applyAlarmTimeoutId is not None:
765                         gobject.source_remove(self._applyAlarmTimeoutId)
766                         self._applyAlarmTimeoutId = None
767                 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
768
769         def _on_minutes_changed(self, *args):
770                 if self._applyAlarmTimeoutId is not None:
771                         gobject.source_remove(self._applyAlarmTimeoutId)
772                         self._applyAlarmTimeoutId = None
773                 self._applyAlarmTimeoutId = gobject.timeout_add(500, self._on_apply_timeout)
774
775         def _on_apply_timeout(self, *args):
776                 self._applyAlarmTimeoutId = None
777
778                 self._update_alarm_settings()
779                 return False
780
781         def _on_missed_toggled(self, *args):
782                 self._notifyOnMissed = self._missedCheckbox.get_active()
783                 self.save_everything()
784
785         def _on_voicemail_toggled(self, *args):
786                 self._notifyOnVoicemail = self._voicemailCheckbox.get_active()
787                 self.save_everything()
788
789         def _on_sms_toggled(self, *args):
790                 self._notifyOnSms = self._smsCheckbox.get_active()
791                 self.save_everything()
792
793
794 class RecentCallsView(object):
795
796         NUMBER_IDX = 0
797         DATE_IDX = 1
798         ACTION_IDX = 2
799         FROM_IDX = 3
800
801         def __init__(self, widgetTree, backend, errorDisplay):
802                 self._errorDisplay = errorDisplay
803                 self._backend = backend
804
805                 self._isPopulated = False
806                 self._recentmodel = gtk.ListStore(
807                         gobject.TYPE_STRING, # number
808                         gobject.TYPE_STRING, # date
809                         gobject.TYPE_STRING, # action
810                         gobject.TYPE_STRING, # from
811                 )
812                 self._recentview = widgetTree.get_widget("recentview")
813                 self._recentviewselection = None
814                 self._onRecentviewRowActivatedId = 0
815
816                 textrenderer = gtk.CellRendererText()
817                 textrenderer.set_property("yalign", 0)
818                 hildonize.set_cell_thumb_selectable(textrenderer)
819                 self._dateColumn = gtk.TreeViewColumn("Date")
820                 self._dateColumn.pack_start(textrenderer, expand=True)
821                 self._dateColumn.add_attribute(textrenderer, "text", self.DATE_IDX)
822
823                 textrenderer = gtk.CellRendererText()
824                 textrenderer.set_property("yalign", 0)
825                 hildonize.set_cell_thumb_selectable(textrenderer)
826                 self._actionColumn = gtk.TreeViewColumn("Action")
827                 self._actionColumn.pack_start(textrenderer, expand=True)
828                 self._actionColumn.add_attribute(textrenderer, "text", self.ACTION_IDX)
829
830                 textrenderer = gtk.CellRendererText()
831                 textrenderer.set_property("yalign", 0)
832                 hildonize.set_cell_thumb_selectable(textrenderer)
833                 self._nameColumn = gtk.TreeViewColumn("From")
834                 self._nameColumn.pack_start(textrenderer, expand=True)
835                 self._nameColumn.add_attribute(textrenderer, "text", self.FROM_IDX)
836                 self._nameColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
837
838                 textrenderer = gtk.CellRendererText()
839                 textrenderer.set_property("yalign", 0)
840                 hildonize.set_cell_thumb_selectable(textrenderer)
841                 self._numberColumn = gtk.TreeViewColumn("Number")
842                 self._numberColumn.pack_start(textrenderer, expand=True)
843                 self._numberColumn.add_attribute(textrenderer, "text", self.NUMBER_IDX)
844
845                 self._window = gtk_toolbox.find_parent_window(self._recentview)
846                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
847
848                 self._updateSink = gtk_toolbox.threaded_stage(
849                         gtk_toolbox.comap(
850                                 self._idly_populate_recentview,
851                                 gtk_toolbox.null_sink(),
852                         )
853                 )
854
855         def enable(self):
856                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
857                 self._recentview.set_model(self._recentmodel)
858
859                 self._recentview.append_column(self._dateColumn)
860                 self._recentview.append_column(self._actionColumn)
861                 self._recentview.append_column(self._numberColumn)
862                 self._recentview.append_column(self._nameColumn)
863                 self._recentviewselection = self._recentview.get_selection()
864                 self._recentviewselection.set_mode(gtk.SELECTION_SINGLE)
865
866                 self._onRecentviewRowActivatedId = self._recentview.connect("row-activated", self._on_recentview_row_activated)
867
868         def disable(self):
869                 self._recentview.disconnect(self._onRecentviewRowActivatedId)
870
871                 self.clear()
872
873                 self._recentview.remove_column(self._dateColumn)
874                 self._recentview.remove_column(self._actionColumn)
875                 self._recentview.remove_column(self._nameColumn)
876                 self._recentview.remove_column(self._numberColumn)
877                 self._recentview.set_model(None)
878
879         def number_selected(self, action, number, message):
880                 """
881                 @note Actual dial function is patched in later
882                 """
883                 raise NotImplementedError("Horrible unknown error has occurred")
884
885         def update(self, force = False):
886                 if not force and self._isPopulated:
887                         return False
888                 self._updateSink.send(())
889                 return True
890
891         def clear(self):
892                 self._isPopulated = False
893                 self._recentmodel.clear()
894
895         @staticmethod
896         def name():
897                 return "Recent Calls"
898
899         def load_settings(self, config, section):
900                 pass
901
902         def save_settings(self, config, section):
903                 """
904                 @note Thread Agnostic
905                 """
906                 pass
907
908         def _idly_populate_recentview(self):
909                 self._recentmodel.clear()
910                 self._isPopulated = True
911
912                 try:
913                         recentItems = self._backend.get_recent()
914                 except StandardError, e:
915                         self._errorDisplay.push_exception_with_lock()
916                         self._isPopulated = False
917                         recentItems = []
918
919                 for personName, phoneNumber, date, action in recentItems:
920                         if not personName:
921                                 personName = "Unknown"
922                         prettyNumber = phoneNumber[2:] if phoneNumber.startswith("+1") else phoneNumber
923                         prettyNumber = make_pretty(prettyNumber)
924                         item = (prettyNumber, date, action.capitalize(), personName)
925                         with gtk_toolbox.gtk_lock():
926                                 self._recentmodel.append(item)
927
928                 return False
929
930         def _on_recentview_row_activated(self, treeview, path, view_column):
931                 model, itr = self._recentviewselection.get_selected()
932                 if not itr:
933                         return
934
935                 number = self._recentmodel.get_value(itr, self.NUMBER_IDX)
936                 number = make_ugly(number)
937                 contactPhoneNumbers = [("Phone", number)]
938                 description = self._recentmodel.get_value(itr, self.FROM_IDX)
939
940                 action, phoneNumber, message = self._phoneTypeSelector.run(
941                         contactPhoneNumbers,
942                         message = description,
943                         parent = self._window,
944                 )
945                 if action == PhoneTypeSelector.ACTION_CANCEL:
946                         return
947                 assert phoneNumber, "A lack of phone number exists"
948
949                 self.number_selected(action, phoneNumber, message)
950                 self._recentviewselection.unselect_all()
951
952
953 class MessagesView(object):
954
955         NUMBER_IDX = 0
956         DATE_IDX = 1
957         HEADER_IDX = 2
958         MESSAGE_IDX = 3
959
960         def __init__(self, widgetTree, backend, errorDisplay):
961                 self._errorDisplay = errorDisplay
962                 self._backend = backend
963
964                 self._isPopulated = False
965                 self._messagemodel = gtk.ListStore(
966                         gobject.TYPE_STRING, # number
967                         gobject.TYPE_STRING, # date
968                         gobject.TYPE_STRING, # header
969                         gobject.TYPE_STRING, # message
970                 )
971                 self._messageview = widgetTree.get_widget("messages_view")
972                 self._messageviewselection = None
973                 self._onMessageviewRowActivatedId = 0
974
975                 self._messageRenderer = gtk.CellRendererText()
976                 hildonize.set_cell_thumb_selectable(self._messageRenderer)
977                 self._messageRenderer.set_property("wrap-mode", pango.WRAP_WORD)
978                 self._messageRenderer.set_property("wrap-width", 650)
979                 self._messageColumn = gtk.TreeViewColumn("Messages")
980                 self._messageColumn.pack_start(self._messageRenderer, expand=True)
981                 self._messageColumn.add_attribute(self._messageRenderer, "markup", self.MESSAGE_IDX)
982                 self._messageColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
983
984                 self._window = gtk_toolbox.find_parent_window(self._messageview)
985                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
986
987                 self._updateSink = gtk_toolbox.threaded_stage(
988                         gtk_toolbox.comap(
989                                 self._idly_populate_messageview,
990                                 gtk_toolbox.null_sink(),
991                         )
992                 )
993
994         def enable(self):
995                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
996                 self._messageview.set_model(self._messagemodel)
997
998                 self._messageview.append_column(self._messageColumn)
999                 self._messageviewselection = self._messageview.get_selection()
1000                 self._messageviewselection.set_mode(gtk.SELECTION_SINGLE)
1001
1002                 self._onMessageviewRowActivatedId = self._messageview.connect("row-activated", self._on_messageview_row_activated)
1003
1004         def disable(self):
1005                 self._messageview.disconnect(self._onMessageviewRowActivatedId)
1006
1007                 self.clear()
1008
1009                 self._messageview.remove_column(self._messageColumn)
1010                 self._messageview.set_model(None)
1011
1012         def number_selected(self, action, number, message):
1013                 """
1014                 @note Actual dial function is patched in later
1015                 """
1016                 raise NotImplementedError("Horrible unknown error has occurred")
1017
1018         def update(self, force = False):
1019                 if not force and self._isPopulated:
1020                         return False
1021                 self._updateSink.send(())
1022                 return True
1023
1024         def clear(self):
1025                 self._isPopulated = False
1026                 self._messagemodel.clear()
1027
1028         @staticmethod
1029         def name():
1030                 return "Messages"
1031
1032         def load_settings(self, config, section):
1033                 pass
1034
1035         def save_settings(self, config, section):
1036                 """
1037                 @note Thread Agnostic
1038                 """
1039                 pass
1040
1041         def _idly_populate_messageview(self):
1042                 self._messagemodel.clear()
1043                 self._isPopulated = True
1044
1045                 try:
1046                         messageItems = self._backend.get_messages()
1047                 except StandardError, e:
1048                         self._errorDisplay.push_exception_with_lock()
1049                         self._isPopulated = False
1050                         messageItems = []
1051
1052                 for header, number, relativeDate, message in messageItems:
1053                         prettyNumber = number[2:] if number.startswith("+1") else number
1054                         prettyNumber = make_pretty(prettyNumber)
1055                         message = "<b>%s - %s</b> <i>(%s)</i>\n\n%s" % (header, prettyNumber, relativeDate, message)
1056                         number = make_ugly(number)
1057                         row = (number, relativeDate, header, message)
1058                         with gtk_toolbox.gtk_lock():
1059                                 self._messagemodel.append(row)
1060
1061                 return False
1062
1063         def _on_messageview_row_activated(self, treeview, path, view_column):
1064                 model, itr = self._messageviewselection.get_selected()
1065                 if not itr:
1066                         return
1067
1068                 contactPhoneNumbers = [("Phone", self._messagemodel.get_value(itr, self.NUMBER_IDX))]
1069                 description = self._messagemodel.get_value(itr, self.MESSAGE_IDX)
1070
1071                 action, phoneNumber, message = self._phoneTypeSelector.run(
1072                         contactPhoneNumbers,
1073                         message = description,
1074                         parent = self._window,
1075                 )
1076                 if action == PhoneTypeSelector.ACTION_CANCEL:
1077                         return
1078                 assert phoneNumber, "A lock of phone number exists"
1079
1080                 self.number_selected(action, phoneNumber, message)
1081                 self._messageviewselection.unselect_all()
1082
1083
1084 class ContactsView(object):
1085
1086         def __init__(self, widgetTree, backend, errorDisplay):
1087                 self._errorDisplay = errorDisplay
1088                 self._backend = backend
1089
1090                 self._addressBook = None
1091                 self._selectedComboIndex = 0
1092                 self._addressBookFactories = [null_backend.NullAddressBook()]
1093
1094                 self._booksList = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1095                 self._booksSelectionBox = widgetTree.get_widget("addressbook_combo")
1096
1097                 self._isPopulated = False
1098                 self._contactsmodel = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
1099                 self._contactsviewselection = None
1100                 self._contactsview = widgetTree.get_widget("contactsview")
1101
1102                 self._contactColumn = gtk.TreeViewColumn("Contact")
1103                 displayContactSource = False
1104                 if displayContactSource:
1105                         textrenderer = gtk.CellRendererText()
1106                         self._contactColumn.pack_start(textrenderer, expand=False)
1107                         self._contactColumn.add_attribute(textrenderer, 'text', 0)
1108                 textrenderer = gtk.CellRendererText()
1109                 hildonize.set_cell_thumb_selectable(textrenderer)
1110                 self._contactColumn.pack_start(textrenderer, expand=True)
1111                 self._contactColumn.add_attribute(textrenderer, 'text', 1)
1112                 textrenderer = gtk.CellRendererText()
1113                 self._contactColumn.pack_start(textrenderer, expand=True)
1114                 self._contactColumn.add_attribute(textrenderer, 'text', 4)
1115                 self._contactColumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
1116                 self._contactColumn.set_sort_column_id(1)
1117                 self._contactColumn.set_visible(True)
1118
1119                 self._onContactsviewRowActivatedId = 0
1120                 self._onAddressbookComboChangedId = 0
1121                 self._window = gtk_toolbox.find_parent_window(self._contactsview)
1122                 self._phoneTypeSelector = PhoneTypeSelector(widgetTree, self._backend)
1123
1124                 self._updateSink = gtk_toolbox.threaded_stage(
1125                         gtk_toolbox.comap(
1126                                 self._idly_populate_contactsview,
1127                                 gtk_toolbox.null_sink(),
1128                         )
1129                 )
1130
1131         def enable(self):
1132                 assert self._backend.is_authed(), "Attempting to enable backend while not logged in"
1133
1134                 self._contactsview.set_model(self._contactsmodel)
1135                 self._contactsview.append_column(self._contactColumn)
1136                 self._contactsviewselection = self._contactsview.get_selection()
1137                 self._contactsviewselection.set_mode(gtk.SELECTION_SINGLE)
1138
1139                 self._booksList.clear()
1140                 for (factoryId, bookId), (factoryName, bookName) in self.get_addressbooks():
1141                         if factoryName and bookName:
1142                                 entryName = "%s: %s" % (factoryName, bookName)
1143                         elif factoryName:
1144                                 entryName = factoryName
1145                         elif bookName:
1146                                 entryName = bookName
1147                         else:
1148                                 entryName = "Bad name (%d)" % factoryId
1149                         row = (str(factoryId), bookId, entryName)
1150                         self._booksList.append(row)
1151
1152                 self._booksSelectionBox.set_model(self._booksList)
1153                 cell = gtk.CellRendererText()
1154                 self._booksSelectionBox.pack_start(cell, True)
1155                 self._booksSelectionBox.add_attribute(cell, 'text', 2)
1156
1157                 self._onContactsviewRowActivatedId = self._contactsview.connect("row-activated", self._on_contactsview_row_activated)
1158                 self._onAddressbookComboChangedId = self._booksSelectionBox.connect("changed", self._on_addressbook_combo_changed)
1159
1160                 if len(self._booksList) <= self._selectedComboIndex:
1161                         self._selectedComboIndex = 0
1162                 self._booksSelectionBox.set_active(self._selectedComboIndex)
1163
1164         def disable(self):
1165                 self._contactsview.disconnect(self._onContactsviewRowActivatedId)
1166                 self._booksSelectionBox.disconnect(self._onAddressbookComboChangedId)
1167
1168                 self.clear()
1169
1170                 self._booksSelectionBox.clear()
1171                 self._booksSelectionBox.set_model(None)
1172                 self._contactsview.set_model(None)
1173                 self._contactsview.remove_column(self._contactColumn)
1174
1175         def number_selected(self, action, number, message):
1176                 """
1177                 @note Actual dial function is patched in later
1178                 """
1179                 raise NotImplementedError("Horrible unknown error has occurred")
1180
1181         def get_addressbooks(self):
1182                 """
1183                 @returns Iterable of ((Factory Id, Book Id), (Factory Name, Book Name))
1184                 """
1185                 for i, factory in enumerate(self._addressBookFactories):
1186                         for bookFactory, bookId, bookName in factory.get_addressbooks():
1187                                 yield (str(i), bookId), (factory.factory_name(), bookName)
1188
1189         def open_addressbook(self, bookFactoryId, bookId):
1190                 bookFactoryIndex = int(bookFactoryId)
1191                 addressBook = self._addressBookFactories[bookFactoryIndex].open_addressbook(bookId)
1192
1193                 forceUpdate = True if addressBook is not self._addressBook else False
1194
1195                 self._addressBook = addressBook
1196                 self.update(force=forceUpdate)
1197
1198         def update(self, force = False):
1199                 if not force and self._isPopulated:
1200                         return False
1201                 self._updateSink.send(())
1202                 return True
1203
1204         def clear(self):
1205                 self._isPopulated = False
1206                 self._contactsmodel.clear()
1207                 for factory in self._addressBookFactories:
1208                         factory.clear_caches()
1209                 self._addressBook.clear_caches()
1210
1211         def append(self, book):
1212                 self._addressBookFactories.append(book)
1213
1214         def extend(self, books):
1215                 self._addressBookFactories.extend(books)
1216
1217         @staticmethod
1218         def name():
1219                 return "Contacts"
1220
1221         def load_settings(self, config, sectionName):
1222                 try:
1223                         self._selectedComboIndex = config.getint(sectionName, "selectedAddressbook")
1224                 except ConfigParser.NoOptionError:
1225                         self._selectedComboIndex = 0
1226
1227         def save_settings(self, config, sectionName):
1228                 config.set(sectionName, "selectedAddressbook", str(self._selectedComboIndex))
1229
1230         def _idly_populate_contactsview(self):
1231                 addressBook = None
1232                 while addressBook is not self._addressBook:
1233                         addressBook = self._addressBook
1234                         with gtk_toolbox.gtk_lock():
1235                                 self._contactsview.set_model(None)
1236                                 self.clear()
1237
1238                         try:
1239                                 contacts = addressBook.get_contacts()
1240                         except StandardError, e:
1241                                 contacts = []
1242                                 self._isPopulated = False
1243                                 self._errorDisplay.push_exception_with_lock()
1244                         for contactId, contactName in contacts:
1245                                 contactType = (addressBook.contact_source_short_name(contactId), )
1246                                 self._contactsmodel.append(contactType + (contactName, "", contactId) + ("", ))
1247
1248                         with gtk_toolbox.gtk_lock():
1249                                 self._contactsview.set_model(self._contactsmodel)
1250
1251                 self._isPopulated = True
1252                 return False
1253
1254         def _on_addressbook_combo_changed(self, *args, **kwds):
1255                 itr = self._booksSelectionBox.get_active_iter()
1256                 if itr is None:
1257                         return
1258                 self._selectedComboIndex = self._booksSelectionBox.get_active()
1259                 selectedFactoryId = self._booksList.get_value(itr, 0)
1260                 selectedBookId = self._booksList.get_value(itr, 1)
1261                 self.open_addressbook(selectedFactoryId, selectedBookId)
1262
1263         def _on_contactsview_row_activated(self, treeview, path, view_column):
1264                 model, itr = self._contactsviewselection.get_selected()
1265                 if not itr:
1266                         return
1267
1268                 contactId = self._contactsmodel.get_value(itr, 3)
1269                 contactName = self._contactsmodel.get_value(itr, 1)
1270                 try:
1271                         contactDetails = self._addressBook.get_contact_details(contactId)
1272                 except StandardError, e:
1273                         contactDetails = []
1274                         self._errorDisplay.push_exception()
1275                 contactPhoneNumbers = [phoneNumber for phoneNumber in contactDetails]
1276
1277                 if len(contactPhoneNumbers) == 0:
1278                         return
1279
1280                 action, phoneNumber, message = self._phoneTypeSelector.run(
1281                         contactPhoneNumbers,
1282                         message = contactName,
1283                         parent = self._window,
1284                 )
1285                 if action == PhoneTypeSelector.ACTION_CANCEL:
1286                         return
1287                 assert phoneNumber, "A lack of phone number exists"
1288
1289                 self.number_selected(action, phoneNumber, message)
1290                 self._contactsviewselection.unselect_all()