Adding orientation to the account dialog
[gc-dialer] / src / dialogs.py
1 #!/usr/bin/env python
2
3 from __future__ import with_statement
4 from __future__ import division
5
6 import functools
7 import copy
8 import logging
9
10 import util.qt_compat as qt_compat
11 QtCore = qt_compat.QtCore
12 QtGui = qt_compat.import_module("QtGui")
13
14 import constants
15 from util import qwrappers
16 from util import qui_utils
17 from util import misc as misc_utils
18
19
20 _moduleLogger = logging.getLogger(__name__)
21
22
23 class CredentialsDialog(object):
24
25         def __init__(self, app):
26                 self._app = app
27                 self._usernameField = QtGui.QLineEdit()
28                 self._passwordField = QtGui.QLineEdit()
29                 self._passwordField.setEchoMode(QtGui.QLineEdit.Password)
30
31                 self._credLayout = QtGui.QGridLayout()
32                 self._credLayout.addWidget(QtGui.QLabel("Username"), 0, 0)
33                 self._credLayout.addWidget(self._usernameField, 0, 1)
34                 self._credLayout.addWidget(QtGui.QLabel("Password"), 1, 0)
35                 self._credLayout.addWidget(self._passwordField, 1, 1)
36
37                 self._loginButton = QtGui.QPushButton("&Login")
38                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
39                 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
40
41                 self._layout = QtGui.QVBoxLayout()
42                 self._layout.addLayout(self._credLayout)
43                 self._layout.addWidget(self._buttonLayout)
44
45                 self._dialog = QtGui.QDialog()
46                 self._dialog.setWindowTitle("Login")
47                 self._dialog.setLayout(self._layout)
48                 self._dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose, False)
49                 self._buttonLayout.accepted.connect(self._dialog.accept)
50                 self._buttonLayout.rejected.connect(self._dialog.reject)
51
52                 self._closeWindowAction = QtGui.QAction(None)
53                 self._closeWindowAction.setText("Close")
54                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
55                 self._closeWindowAction.triggered.connect(self._on_close_window)
56
57                 self._dialog.addAction(self._closeWindowAction)
58                 self._dialog.addAction(app.quitAction)
59                 self._dialog.addAction(app.fullscreenAction)
60
61         def run(self, defaultUsername, defaultPassword, parent=None):
62                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
63                 try:
64                         self._usernameField.setText(defaultUsername)
65                         self._passwordField.setText(defaultPassword)
66
67                         response = self._dialog.exec_()
68                         if response == QtGui.QDialog.Accepted:
69                                 return str(self._usernameField.text()), str(self._passwordField.text())
70                         elif response == QtGui.QDialog.Rejected:
71                                 return None
72                         else:
73                                 _moduleLogger.error("Unknown response")
74                                 return None
75                 finally:
76                         self._dialog.setParent(None, QtCore.Qt.Dialog)
77
78         def close(self):
79                 try:
80                         self._dialog.reject()
81                 except RuntimeError:
82                         _moduleLogger.exception("Oh well")
83
84         @qt_compat.Slot()
85         @qt_compat.Slot(bool)
86         @misc_utils.log_exception(_moduleLogger)
87         def _on_close_window(self, checked = True):
88                 with qui_utils.notify_error(self._app.errorLog):
89                         self._dialog.reject()
90
91
92 class AboutDialog(object):
93
94         def __init__(self, app):
95                 self._app = app
96                 self._title = QtGui.QLabel(
97                         "<h1>%s</h1><h3>Version: %s</h3>" % (
98                                 constants.__pretty_app_name__, constants.__version__
99                         )
100                 )
101                 self._title.setTextFormat(QtCore.Qt.RichText)
102                 self._title.setAlignment(QtCore.Qt.AlignCenter)
103                 self._copyright = QtGui.QLabel("<h6>Developed by Ed Page<h6><h6>Icons: See website</h6>")
104                 self._copyright.setTextFormat(QtCore.Qt.RichText)
105                 self._copyright.setAlignment(QtCore.Qt.AlignCenter)
106                 self._link = QtGui.QLabel('<a href="http://gc-dialer.garage.maemo.org">DialCentral Website</a>')
107                 self._link.setTextFormat(QtCore.Qt.RichText)
108                 self._link.setAlignment(QtCore.Qt.AlignCenter)
109                 self._link.setOpenExternalLinks(True)
110
111                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
112
113                 self._layout = QtGui.QVBoxLayout()
114                 self._layout.addWidget(self._title)
115                 self._layout.addWidget(self._copyright)
116                 self._layout.addWidget(self._link)
117                 self._layout.addWidget(self._buttonLayout)
118
119                 self._dialog = QtGui.QDialog()
120                 self._dialog.setWindowTitle("About")
121                 self._dialog.setLayout(self._layout)
122                 self._buttonLayout.rejected.connect(self._dialog.reject)
123
124                 self._closeWindowAction = QtGui.QAction(None)
125                 self._closeWindowAction.setText("Close")
126                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
127                 self._closeWindowAction.triggered.connect(self._on_close_window)
128
129                 self._dialog.addAction(self._closeWindowAction)
130                 self._dialog.addAction(app.quitAction)
131                 self._dialog.addAction(app.fullscreenAction)
132
133         def run(self, parent=None):
134                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
135
136                 response = self._dialog.exec_()
137                 return response
138
139         def close(self):
140                 try:
141                         self._dialog.reject()
142                 except RuntimeError:
143                         _moduleLogger.exception("Oh well")
144
145         @qt_compat.Slot()
146         @qt_compat.Slot(bool)
147         @misc_utils.log_exception(_moduleLogger)
148         def _on_close_window(self, checked = True):
149                 with qui_utils.notify_error(self._app.errorLog):
150                         self._dialog.reject()
151
152
153 class AccountDialog(object):
154
155         # @bug Can't enter custom callback numbers
156
157         _RECURRENCE_CHOICES = [
158                 (1, "1 minute"),
159                 (2, "2 minutes"),
160                 (3, "3 minutes"),
161                 (5, "5 minutes"),
162                 (8, "8 minutes"),
163                 (10, "10 minutes"),
164                 (15, "15 minutes"),
165                 (30, "30 minutes"),
166                 (45, "45 minutes"),
167                 (60, "1 hour"),
168                 (3*60, "3 hours"),
169                 (6*60, "6 hours"),
170                 (12*60, "12 hours"),
171         ]
172
173         ALARM_NONE = "No Alert"
174         ALARM_BACKGROUND = "Background Alert"
175         ALARM_APPLICATION = "Application Alert"
176
177         VOICEMAIL_CHECK_NOT_SUPPORTED = "Not Supported"
178         VOICEMAIL_CHECK_DISABLED = "Disabled"
179         VOICEMAIL_CHECK_ENABLED = "Enabled"
180
181         def __init__(self, app):
182                 self._app = app
183                 self._doClear = False
184
185                 self._accountNumberLabel = QtGui.QLabel("NUMBER NOT SET")
186                 self._notificationSelecter = QtGui.QComboBox()
187                 self._notificationSelecter.currentIndexChanged.connect(self._on_notification_change)
188                 self._notificationTimeSelector = QtGui.QComboBox()
189                 #self._notificationTimeSelector.setEditable(True)
190                 self._notificationTimeSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
191                 for _, label in self._RECURRENCE_CHOICES:
192                         self._notificationTimeSelector.addItem(label)
193                 self._missedCallsNotificationButton = QtGui.QCheckBox("Missed Calls")
194                 self._voicemailNotificationButton = QtGui.QCheckBox("Voicemail")
195                 self._smsNotificationButton = QtGui.QCheckBox("SMS")
196                 self._voicemailOnMissedButton = QtGui.QCheckBox("Voicemail Update on Missed Calls")
197                 self._clearButton = QtGui.QPushButton("Clear Account")
198                 self._clearButton.clicked.connect(self._on_clear)
199                 self._callbackSelector = QtGui.QComboBox()
200                 #self._callbackSelector.setEditable(True)
201                 self._callbackSelector.setInsertPolicy(QtGui.QComboBox.InsertAtTop)
202                 self._orientationSelector = QtGui.QComboBox()
203                 for orientationMode in [
204                         self._app.DEFAULT_ORIENTATION,
205                         self._app.AUTO_ORIENTATION,
206                         self._app.LANDSCAPE_ORIENTATION,
207                         self._app.PORTRAIT_ORIENTATION,
208                 ]:
209                         self._orientationSelector.addItem(orientationMode)
210
211                 self._update_notification_state()
212
213                 self._credLayout = QtGui.QGridLayout()
214                 self._credLayout.addWidget(QtGui.QLabel("Account"), 0, 0)
215                 self._credLayout.addWidget(self._accountNumberLabel, 0, 1)
216                 self._credLayout.addWidget(QtGui.QLabel("Callback"), 1, 0)
217                 self._credLayout.addWidget(self._callbackSelector, 1, 1)
218                 self._credLayout.addWidget(self._notificationSelecter, 2, 0)
219                 self._credLayout.addWidget(self._notificationTimeSelector, 2, 1)
220                 self._credLayout.addWidget(QtGui.QLabel(""), 3, 0)
221                 self._credLayout.addWidget(self._missedCallsNotificationButton, 3, 1)
222                 self._credLayout.addWidget(QtGui.QLabel(""), 4, 0)
223                 self._credLayout.addWidget(self._voicemailNotificationButton, 4, 1)
224                 self._credLayout.addWidget(QtGui.QLabel(""), 5, 0)
225                 self._credLayout.addWidget(self._smsNotificationButton, 5, 1)
226                 self._credLayout.addWidget(QtGui.QLabel("Other"), 6, 0)
227                 self._credLayout.addWidget(self._voicemailOnMissedButton, 6, 1)
228                 self._credLayout.addWidget(QtGui.QLabel("Orientation"), 7, 0)
229                 self._credLayout.addWidget(self._orientationSelector, 7, 1)
230                 self._credLayout.addWidget(QtGui.QLabel(""), 8, 0)
231                 self._credLayout.addWidget(QtGui.QLabel(""), 9, 0)
232                 self._credLayout.addWidget(self._clearButton, 9, 1)
233
234                 self._credWidget = QtGui.QWidget()
235                 self._credWidget.setLayout(self._credLayout)
236                 self._credWidget.setContentsMargins(0, 0, 0, 0)
237                 self._scrollSettings = QtGui.QScrollArea()
238                 self._scrollSettings.setWidget(self._credWidget)
239                 self._scrollSettings.setWidgetResizable(True)
240                 self._scrollSettings.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
241                 self._scrollSettings.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
242
243                 self._loginButton = QtGui.QPushButton("&Apply")
244                 self._buttonLayout = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Cancel)
245                 self._buttonLayout.addButton(self._loginButton, QtGui.QDialogButtonBox.AcceptRole)
246
247                 self._layout = QtGui.QVBoxLayout()
248                 self._layout.addWidget(self._scrollSettings)
249                 self._layout.addWidget(self._buttonLayout)
250
251                 self._dialog = QtGui.QDialog()
252                 self._dialog.setWindowTitle("Account")
253                 self._dialog.setLayout(self._layout)
254                 self._buttonLayout.accepted.connect(self._dialog.accept)
255                 self._buttonLayout.rejected.connect(self._dialog.reject)
256
257                 self._closeWindowAction = QtGui.QAction(None)
258                 self._closeWindowAction.setText("Close")
259                 self._closeWindowAction.setShortcut(QtGui.QKeySequence("CTRL+w"))
260                 self._closeWindowAction.triggered.connect(self._on_close_window)
261
262                 self._dialog.addAction(self._closeWindowAction)
263                 self._dialog.addAction(app.quitAction)
264                 self._dialog.addAction(app.fullscreenAction)
265
266         @property
267         def doClear(self):
268                 return self._doClear
269
270         def setIfNotificationsSupported(self, isSupported):
271                 if isSupported:
272                         self._notificationSelecter.clear()
273                         self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION, self.ALARM_BACKGROUND])
274                         self._notificationTimeSelector.setEnabled(False)
275                         self._missedCallsNotificationButton.setEnabled(False)
276                         self._voicemailNotificationButton.setEnabled(False)
277                         self._smsNotificationButton.setEnabled(False)
278                 else:
279                         self._notificationSelecter.clear()
280                         self._notificationSelecter.addItems([self.ALARM_NONE, self.ALARM_APPLICATION])
281                         self._notificationTimeSelector.setEnabled(False)
282                         self._missedCallsNotificationButton.setEnabled(False)
283                         self._voicemailNotificationButton.setEnabled(False)
284                         self._smsNotificationButton.setEnabled(False)
285
286         def set_account_number(self, num):
287                 self._accountNumberLabel.setText(num)
288
289         orientation = property(
290                 lambda self: str(self._orientationSelector.currentText()),
291                 lambda self, mode: qui_utils.set_current_index(self._orientationSelector, mode),
292         )
293
294         def _set_voicemail_on_missed(self, status):
295                 if status == self.VOICEMAIL_CHECK_NOT_SUPPORTED:
296                         self._voicemailOnMissedButton.setChecked(False)
297                         self._voicemailOnMissedButton.hide()
298                 elif status == self.VOICEMAIL_CHECK_DISABLED:
299                         self._voicemailOnMissedButton.setChecked(False)
300                         self._voicemailOnMissedButton.show()
301                 elif status == self.VOICEMAIL_CHECK_ENABLED:
302                         self._voicemailOnMissedButton.setChecked(True)
303                         self._voicemailOnMissedButton.show()
304                 else:
305                         raise RuntimeError("Unsupported option for updating voicemail on missed calls %r" % status)
306
307         def _get_voicemail_on_missed(self):
308                 if not self._voicemailOnMissedButton.isVisible():
309                         return self.VOICEMAIL_CHECK_NOT_SUPPORTED
310                 elif self._voicemailOnMissedButton.isChecked():
311                         return self.VOICEMAIL_CHECK_ENABLED
312                 else:
313                         return self.VOICEMAIL_CHECK_DISABLED
314
315         updateVMOnMissedCall = property(_get_voicemail_on_missed, _set_voicemail_on_missed)
316
317         notifications = property(
318                 lambda self: str(self._notificationSelecter.currentText()),
319                 lambda self, enabled: qui_utils.set_current_index(self._notificationSelecter, enabled),
320         )
321
322         notifyOnMissed = property(
323                 lambda self: self._missedCallsNotificationButton.isChecked(),
324                 lambda self, enabled: self._missedCallsNotificationButton.setChecked(enabled),
325         )
326
327         notifyOnVoicemail = property(
328                 lambda self: self._voicemailNotificationButton.isChecked(),
329                 lambda self, enabled: self._voicemailNotificationButton.setChecked(enabled),
330         )
331
332         notifyOnSms = property(
333                 lambda self: self._smsNotificationButton.isChecked(),
334                 lambda self, enabled: self._smsNotificationButton.setChecked(enabled),
335         )
336
337         def _get_notification_time(self):
338                 index = self._notificationTimeSelector.currentIndex()
339                 minutes = self._RECURRENCE_CHOICES[index][0]
340                 return minutes
341
342         def _set_notification_time(self, minutes):
343                 for i, (time, _) in enumerate(self._RECURRENCE_CHOICES):
344                         if time == minutes:
345                                 self._notificationTimeSelector.setCurrentIndex(i)
346                                 break
347                 else:
348                                 self._notificationTimeSelector.setCurrentIndex(0)
349
350         notificationTime = property(_get_notification_time, _set_notification_time)
351
352         @property
353         def selectedCallback(self):
354                 index = self._callbackSelector.currentIndex()
355                 data = str(self._callbackSelector.itemData(index))
356                 return data
357
358         def set_callbacks(self, choices, default):
359                 self._callbackSelector.clear()
360
361                 self._callbackSelector.addItem("Not Set", "")
362
363                 uglyDefault = misc_utils.make_ugly(default)
364                 for number, description in choices.iteritems():
365                         prettyNumber = misc_utils.make_pretty(number)
366                         uglyNumber = misc_utils.make_ugly(number)
367                         if not uglyNumber:
368                                 continue
369
370                         self._callbackSelector.addItem("%s - %s" % (prettyNumber, description), uglyNumber)
371                         if uglyNumber == uglyDefault:
372                                 self._callbackSelector.setCurrentIndex(self._callbackSelector.count() - 1)
373
374         def run(self, parent=None):
375                 self._doClear = False
376                 self._dialog.setParent(parent, QtCore.Qt.Dialog)
377
378                 response = self._dialog.exec_()
379                 return response
380
381         def close(self):
382                 try:
383                         self._dialog.reject()
384                 except RuntimeError:
385                         _moduleLogger.exception("Oh well")
386
387         def _update_notification_state(self):
388                 currentText = str(self._notificationSelecter.currentText())
389                 if currentText == self.ALARM_BACKGROUND:
390                         self._notificationTimeSelector.setEnabled(True)
391
392                         self._missedCallsNotificationButton.setEnabled(True)
393                         self._voicemailNotificationButton.setEnabled(True)
394                         self._smsNotificationButton.setEnabled(True)
395                 elif currentText == self.ALARM_APPLICATION:
396                         self._notificationTimeSelector.setEnabled(True)
397
398                         self._missedCallsNotificationButton.setEnabled(False)
399                         self._voicemailNotificationButton.setEnabled(True)
400                         self._smsNotificationButton.setEnabled(True)
401
402                         self._missedCallsNotificationButton.setChecked(False)
403                 else:
404                         self._notificationTimeSelector.setEnabled(False)
405
406                         self._missedCallsNotificationButton.setEnabled(False)
407                         self._voicemailNotificationButton.setEnabled(False)
408                         self._smsNotificationButton.setEnabled(False)
409
410                         self._missedCallsNotificationButton.setChecked(False)
411                         self._voicemailNotificationButton.setChecked(False)
412                         self._smsNotificationButton.setChecked(False)
413
414         @qt_compat.Slot(int)
415         @misc_utils.log_exception(_moduleLogger)
416         def _on_notification_change(self, index):
417                 with qui_utils.notify_error(self._app.errorLog):
418                         self._update_notification_state()
419
420         @qt_compat.Slot()
421         @qt_compat.Slot(bool)
422         @misc_utils.log_exception(_moduleLogger)
423         def _on_clear(self, checked = False):
424                 with qui_utils.notify_error(self._app.errorLog):
425                         self._doClear = True
426                         self._dialog.accept()
427
428         @qt_compat.Slot()
429         @qt_compat.Slot(bool)
430         @misc_utils.log_exception(_moduleLogger)
431         def _on_close_window(self, checked = True):
432                 with qui_utils.notify_error(self._app.errorLog):
433                         self._dialog.reject()
434
435
436 class ContactList(object):
437
438         _SENTINEL_ICON = QtGui.QIcon()
439
440         def __init__(self, app, session):
441                 self._app = app
442                 self._session = session
443                 self._targetLayout = QtGui.QVBoxLayout()
444                 self._targetList = QtGui.QWidget()
445                 self._targetList.setLayout(self._targetLayout)
446                 self._uiItems = []
447                 self._closeIcon = qui_utils.get_theme_icon(("window-close", "general_close", "gtk-close"), self._SENTINEL_ICON)
448
449         @property
450         def toplevel(self):
451                 return self._targetList
452
453         def setVisible(self, isVisible):
454                 self._targetList.setVisible(isVisible)
455
456         def update(self):
457                 cids = list(self._session.draft.get_contacts())
458                 amountCommon = min(len(cids), len(self._uiItems))
459
460                 # Run through everything in common
461                 for i in xrange(0, amountCommon):
462                         cid = cids[i]
463                         uiItem = self._uiItems[i]
464                         title = self._session.draft.get_title(cid)
465                         description = self._session.draft.get_description(cid)
466                         numbers = self._session.draft.get_numbers(cid)
467                         uiItem["cid"] = cid
468                         uiItem["title"] = title
469                         uiItem["description"] = description
470                         uiItem["numbers"] = numbers
471                         uiItem["label"].setText(title)
472                         self._populate_number_selector(uiItem["selector"], cid, i, numbers)
473                         uiItem["rowWidget"].setVisible(True)
474
475                 # More contacts than ui items
476                 for i in xrange(amountCommon, len(cids)):
477                         cid = cids[i]
478                         title = self._session.draft.get_title(cid)
479                         description = self._session.draft.get_description(cid)
480                         numbers = self._session.draft.get_numbers(cid)
481
482                         titleLabel = QtGui.QLabel(title)
483                         titleLabel.setWordWrap(True)
484                         numberSelector = QtGui.QComboBox()
485                         self._populate_number_selector(numberSelector, cid, i, numbers)
486
487                         callback = functools.partial(
488                                 self._on_change_number,
489                                 i
490                         )
491                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
492                         numberSelector.activated.connect(
493                                 qt_compat.Slot(int)(callback)
494                         )
495
496                         if self._closeIcon is self._SENTINEL_ICON:
497                                 deleteButton = QtGui.QPushButton("Delete")
498                         else:
499                                 deleteButton = QtGui.QPushButton(self._closeIcon, "")
500                         deleteButton.setSizePolicy(QtGui.QSizePolicy(
501                                 QtGui.QSizePolicy.Minimum,
502                                 QtGui.QSizePolicy.Minimum,
503                                 QtGui.QSizePolicy.PushButton,
504                         ))
505                         callback = functools.partial(
506                                 self._on_remove_contact,
507                                 i
508                         )
509                         callback.__name__ = "thanks partials for not having names and pyqt for requiring them"
510                         deleteButton.clicked.connect(callback)
511
512                         rowLayout = QtGui.QHBoxLayout()
513                         rowLayout.addWidget(titleLabel, 1000)
514                         rowLayout.addWidget(numberSelector, 0)
515                         rowLayout.addWidget(deleteButton, 0)
516                         rowWidget = QtGui.QWidget()
517                         rowWidget.setLayout(rowLayout)
518                         self._targetLayout.addWidget(rowWidget)
519
520                         uiItem = {}
521                         uiItem["cid"] = cid
522                         uiItem["title"] = title
523                         uiItem["description"] = description
524                         uiItem["numbers"] = numbers
525                         uiItem["label"] = titleLabel
526                         uiItem["selector"] = numberSelector
527                         uiItem["rowWidget"] = rowWidget
528                         self._uiItems.append(uiItem)
529                         amountCommon = i+1
530
531                 # More UI items than contacts
532                 for i in xrange(amountCommon, len(self._uiItems)):
533                         uiItem = self._uiItems[i]
534                         uiItem["rowWidget"].setVisible(False)
535                         amountCommon = i+1
536
537         def _populate_number_selector(self, selector, cid, cidIndex, numbers):
538                 selector.clear()
539
540                 selectedNumber = self._session.draft.get_selected_number(cid)
541                 if len(numbers) == 1:
542                         # If no alt numbers available, check the address book
543                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
544                 else:
545                         defaultIndex = _index_number(numbers, selectedNumber)
546
547                 for number, description in numbers:
548                         if description:
549                                 label = "%s - %s" % (number, description)
550                         else:
551                                 label = number
552                         selector.addItem(label)
553                 selector.setVisible(True)
554                 if 1 < len(numbers):
555                         selector.setEnabled(True)
556                         selector.setCurrentIndex(defaultIndex)
557                 else:
558                         selector.setEnabled(False)
559
560         @misc_utils.log_exception(_moduleLogger)
561         def _on_change_number(self, cidIndex, index):
562                 with qui_utils.notify_error(self._app.errorLog):
563                         # Exception thrown when the first item is removed
564                         try:
565                                 cid = self._uiItems[cidIndex]["cid"]
566                                 numbers = self._session.draft.get_numbers(cid)
567                         except IndexError:
568                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
569                                 return
570                         except KeyError:
571                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
572                                 return
573                         number = numbers[index][0]
574                         self._session.draft.set_selected_number(cid, number)
575
576         @misc_utils.log_exception(_moduleLogger)
577         def _on_remove_contact(self, index, toggled):
578                 with qui_utils.notify_error(self._app.errorLog):
579                         self._session.draft.remove_contact(self._uiItems[index]["cid"])
580
581
582 class VoicemailPlayer(object):
583
584         def __init__(self, app, session, errorLog):
585                 self._app = app
586                 self._session = session
587                 self._errorLog = errorLog
588                 self._token = None
589                 self._session.voicemailAvailable.connect(self._on_voicemail_downloaded)
590                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
591
592                 self._playButton = QtGui.QPushButton("Play")
593                 self._playButton.clicked.connect(self._on_voicemail_play)
594                 self._pauseButton = QtGui.QPushButton("Pause")
595                 self._pauseButton.clicked.connect(self._on_voicemail_pause)
596                 self._pauseButton.hide()
597                 self._resumeButton = QtGui.QPushButton("Resume")
598                 self._resumeButton.clicked.connect(self._on_voicemail_resume)
599                 self._resumeButton.hide()
600                 self._stopButton = QtGui.QPushButton("Stop")
601                 self._stopButton.clicked.connect(self._on_voicemail_stop)
602                 self._stopButton.hide()
603
604                 self._downloadButton = QtGui.QPushButton("Download Voicemail")
605                 self._downloadButton.clicked.connect(self._on_voicemail_download)
606                 self._downloadLayout = QtGui.QHBoxLayout()
607                 self._downloadLayout.addWidget(self._downloadButton)
608                 self._downloadWidget = QtGui.QWidget()
609                 self._downloadWidget.setLayout(self._downloadLayout)
610
611                 self._playLabel = QtGui.QLabel("Voicemail")
612                 self._saveButton = QtGui.QPushButton("Save")
613                 self._saveButton.clicked.connect(self._on_voicemail_save)
614                 self._playerLayout = QtGui.QHBoxLayout()
615                 self._playerLayout.addWidget(self._playLabel)
616                 self._playerLayout.addWidget(self._playButton)
617                 self._playerLayout.addWidget(self._pauseButton)
618                 self._playerLayout.addWidget(self._resumeButton)
619                 self._playerLayout.addWidget(self._stopButton)
620                 self._playerLayout.addWidget(self._saveButton)
621                 self._playerWidget = QtGui.QWidget()
622                 self._playerWidget.setLayout(self._playerLayout)
623
624                 self._visibleWidget = None
625                 self._layout = QtGui.QHBoxLayout()
626                 self._layout.setContentsMargins(0, 0, 0, 0)
627                 self._widget = QtGui.QWidget()
628                 self._widget.setLayout(self._layout)
629                 self._update_state()
630
631         @property
632         def toplevel(self):
633                 return self._widget
634
635         def destroy(self):
636                 self._session.voicemailAvailable.disconnect(self._on_voicemail_downloaded)
637                 self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
638                 self._invalidate_token()
639
640         def _invalidate_token(self):
641                 if self._token is not None:
642                         self._token.invalidate()
643                         self._token.error.disconnect(self._on_play_error)
644                         self._token.stateChange.connect(self._on_play_state)
645                         self._token.invalidated.connect(self._on_play_invalidated)
646
647         def _show_download(self, messageId):
648                 if self._visibleWidget is self._downloadWidget:
649                         return
650                 self._hide()
651                 self._layout.addWidget(self._downloadWidget)
652                 self._visibleWidget = self._downloadWidget
653                 self._visibleWidget.show()
654
655         def _show_player(self, messageId):
656                 if self._visibleWidget is self._playerWidget:
657                         return
658                 self._hide()
659                 self._layout.addWidget(self._playerWidget)
660                 self._visibleWidget = self._playerWidget
661                 self._visibleWidget.show()
662
663         def _hide(self):
664                 if self._visibleWidget is None:
665                         return
666                 self._visibleWidget.hide()
667                 self._layout.removeWidget(self._visibleWidget)
668                 self._visibleWidget = None
669
670         def _update_play_state(self):
671                 if self._token is not None and self._token.isValid:
672                         self._playButton.setText("Stop")
673                 else:
674                         self._playButton.setText("Play")
675
676         def _update_state(self):
677                 if self._session.draft.get_num_contacts() != 1:
678                         self._hide()
679                         return
680
681                 (cid, ) = self._session.draft.get_contacts()
682                 messageId = self._session.draft.get_message_id(cid)
683                 if messageId is None:
684                         self._hide()
685                         return
686
687                 if self._session.is_available(messageId):
688                         self._show_player(messageId)
689                 else:
690                         self._show_download(messageId)
691                 if self._token is not None:
692                         self._token.invalidate()
693
694         @misc_utils.log_exception(_moduleLogger)
695         def _on_voicemail_save(self, arg):
696                 with qui_utils.notify_error(self._app.errorLog):
697                         targetPath = QtGui.QFileDialog.getSaveFileName(None, caption="Save Voicemail", filter="Audio File (*.mp3)")
698                         targetPath = unicode(targetPath)
699                         if not targetPath:
700                                 return
701
702                         (cid, ) = self._session.draft.get_contacts()
703                         messageId = self._session.draft.get_message_id(cid)
704                         sourcePath = self._session.voicemail_path(messageId)
705                         import shutil
706                         shutil.copy2(sourcePath, targetPath)
707
708         @misc_utils.log_exception(_moduleLogger)
709         def _on_play_error(self, error):
710                 with qui_utils.notify_error(self._app.errorLog):
711                         self._app.errorLog.push_error(error)
712
713         @misc_utils.log_exception(_moduleLogger)
714         def _on_play_invalidated(self):
715                 with qui_utils.notify_error(self._app.errorLog):
716                         self._playButton.show()
717                         self._pauseButton.hide()
718                         self._resumeButton.hide()
719                         self._stopButton.hide()
720                         self._invalidate_token()
721
722         @misc_utils.log_exception(_moduleLogger)
723         def _on_play_state(self, state):
724                 with qui_utils.notify_error(self._app.errorLog):
725                         if state == self._token.STATE_PLAY:
726                                 self._playButton.hide()
727                                 self._pauseButton.show()
728                                 self._resumeButton.hide()
729                                 self._stopButton.show()
730                         elif state == self._token.STATE_PAUSE:
731                                 self._playButton.hide()
732                                 self._pauseButton.hide()
733                                 self._resumeButton.show()
734                                 self._stopButton.show()
735                         elif state == self._token.STATE_STOP:
736                                 self._playButton.show()
737                                 self._pauseButton.hide()
738                                 self._resumeButton.hide()
739                                 self._stopButton.hide()
740
741         @misc_utils.log_exception(_moduleLogger)
742         def _on_voicemail_play(self, arg):
743                 with qui_utils.notify_error(self._app.errorLog):
744                         (cid, ) = self._session.draft.get_contacts()
745                         messageId = self._session.draft.get_message_id(cid)
746                         sourcePath = self._session.voicemail_path(messageId)
747
748                         self._invalidate_token()
749                         uri = "file://%s" % sourcePath
750                         self._token = self._app.streamHandler.set_file(uri)
751                         self._token.stateChange.connect(self._on_play_state)
752                         self._token.invalidated.connect(self._on_play_invalidated)
753                         self._token.error.connect(self._on_play_error)
754                         self._token.play()
755
756         @misc_utils.log_exception(_moduleLogger)
757         def _on_voicemail_pause(self, arg):
758                 with qui_utils.notify_error(self._app.errorLog):
759                         self._token.pause()
760
761         @misc_utils.log_exception(_moduleLogger)
762         def _on_voicemail_resume(self, arg):
763                 with qui_utils.notify_error(self._app.errorLog):
764                         self._token.play()
765
766         @misc_utils.log_exception(_moduleLogger)
767         def _on_voicemail_stop(self, arg):
768                 with qui_utils.notify_error(self._app.errorLog):
769                         self._token.stop()
770
771         @misc_utils.log_exception(_moduleLogger)
772         def _on_voicemail_download(self, arg):
773                 with qui_utils.notify_error(self._app.errorLog):
774                         (cid, ) = self._session.draft.get_contacts()
775                         messageId = self._session.draft.get_message_id(cid)
776                         self._session.download_voicemail(messageId)
777                         self._hide()
778
779         @qt_compat.Slot()
780         @misc_utils.log_exception(_moduleLogger)
781         def _on_recipients_changed(self):
782                 with qui_utils.notify_error(self._app.errorLog):
783                         self._update_state()
784
785         @qt_compat.Slot(str, str)
786         @misc_utils.log_exception(_moduleLogger)
787         def _on_voicemail_downloaded(self, messageId, filepath):
788                 with qui_utils.notify_error(self._app.errorLog):
789                         self._update_state()
790
791
792 class SMSEntryWindow(qwrappers.WindowWrapper):
793
794         MAX_CHAR = 160
795         # @bug Somehow a window is being destroyed on object creation which causes glitches on Maemo 5
796
797         def __init__(self, parent, app, session, errorLog):
798                 qwrappers.WindowWrapper.__init__(self, parent, app)
799                 self._session = session
800                 self._session.messagesUpdated.connect(self._on_refresh_history)
801                 self._session.historyUpdated.connect(self._on_refresh_history)
802                 self._session.draft.recipientsChanged.connect(self._on_recipients_changed)
803
804                 self._session.draft.sendingMessage.connect(self._on_op_started)
805                 self._session.draft.calling.connect(self._on_op_started)
806                 self._session.draft.calling.connect(self._on_calling_started)
807                 self._session.draft.cancelling.connect(self._on_op_started)
808
809                 self._session.draft.sentMessage.connect(self._on_op_finished)
810                 self._session.draft.called.connect(self._on_op_finished)
811                 self._session.draft.cancelled.connect(self._on_op_finished)
812                 self._session.draft.error.connect(self._on_op_error)
813                 self._errorLog = errorLog
814
815                 self._errorDisplay = qui_utils.ErrorDisplay(self._errorLog)
816
817                 self._targetList = ContactList(self._app, self._session)
818                 self._history = QtGui.QLabel()
819                 self._history.setTextFormat(QtCore.Qt.RichText)
820                 self._history.setWordWrap(True)
821                 self._voicemailPlayer = VoicemailPlayer(self._app, self._session, self._errorLog)
822                 self._smsEntry = QtGui.QTextEdit()
823                 self._smsEntry.textChanged.connect(self._on_letter_count_changed)
824
825                 self._entryLayout = QtGui.QVBoxLayout()
826                 self._entryLayout.addWidget(self._targetList.toplevel)
827                 self._entryLayout.addWidget(self._history)
828                 self._entryLayout.addWidget(self._voicemailPlayer.toplevel, 0)
829                 self._entryLayout.addWidget(self._smsEntry)
830                 self._entryLayout.setContentsMargins(0, 0, 0, 0)
831                 self._entryWidget = QtGui.QWidget()
832                 self._entryWidget.setLayout(self._entryLayout)
833                 self._entryWidget.setContentsMargins(0, 0, 0, 0)
834                 self._scrollEntry = QtGui.QScrollArea()
835                 self._scrollEntry.setWidget(self._entryWidget)
836                 self._scrollEntry.setWidgetResizable(True)
837                 self._scrollEntry.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom)
838                 self._scrollEntry.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
839                 self._scrollEntry.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
840
841                 self._characterCountLabel = QtGui.QLabel("")
842                 self._singleNumberSelector = QtGui.QComboBox()
843                 self._cids = []
844                 self._singleNumberSelector.activated.connect(self._on_single_change_number)
845                 self._smsButton = QtGui.QPushButton("SMS")
846                 self._smsButton.clicked.connect(self._on_sms_clicked)
847                 self._smsButton.setEnabled(False)
848                 self._dialButton = QtGui.QPushButton("Dial")
849                 self._dialButton.clicked.connect(self._on_call_clicked)
850                 self._cancelButton = QtGui.QPushButton("Cancel Call")
851                 self._cancelButton.clicked.connect(self._on_cancel_clicked)
852                 self._cancelButton.setVisible(False)
853
854                 self._buttonLayout = QtGui.QHBoxLayout()
855                 self._buttonLayout.addWidget(self._characterCountLabel)
856                 self._buttonLayout.addWidget(self._singleNumberSelector)
857                 self._buttonLayout.addWidget(self._smsButton)
858                 self._buttonLayout.addWidget(self._dialButton)
859                 self._buttonLayout.addWidget(self._cancelButton)
860
861                 self._layout.addWidget(self._errorDisplay.toplevel)
862                 self._layout.addWidget(self._scrollEntry)
863                 self._layout.addLayout(self._buttonLayout)
864                 self._layout.setDirection(QtGui.QBoxLayout.TopToBottom)
865
866                 self._window.setWindowTitle("Contact")
867                 self._window.closed.connect(self._on_close_window)
868                 self._window.hidden.connect(self._on_close_window)
869                 self._window.resized.connect(self._on_window_resized)
870
871                 self._scrollTimer = QtCore.QTimer()
872                 self._scrollTimer.setInterval(100)
873                 self._scrollTimer.setSingleShot(True)
874                 self._scrollTimer.timeout.connect(self._on_delayed_scroll_to_bottom)
875
876                 self._smsEntry.setPlainText(self._session.draft.message)
877                 self._update_letter_count()
878                 self._update_target_fields()
879                 self.set_fullscreen(self._app.fullscreenAction.isChecked())
880                 self.update_orientation(self._app.orientation)
881
882         def close(self):
883                 if self._window is None:
884                         # Already closed
885                         return
886                 window = self._window
887                 try:
888                         message = unicode(self._smsEntry.toPlainText())
889                         self._session.draft.message = message
890                         self.hide()
891                 except AttributeError:
892                         _moduleLogger.exception("Oh well")
893                 except RuntimeError:
894                         _moduleLogger.exception("Oh well")
895
896         def destroy(self):
897                 self._session.messagesUpdated.disconnect(self._on_refresh_history)
898                 self._session.historyUpdated.disconnect(self._on_refresh_history)
899                 self._session.draft.recipientsChanged.disconnect(self._on_recipients_changed)
900                 self._session.draft.sendingMessage.disconnect(self._on_op_started)
901                 self._session.draft.calling.disconnect(self._on_op_started)
902                 self._session.draft.calling.disconnect(self._on_calling_started)
903                 self._session.draft.cancelling.disconnect(self._on_op_started)
904                 self._session.draft.sentMessage.disconnect(self._on_op_finished)
905                 self._session.draft.called.disconnect(self._on_op_finished)
906                 self._session.draft.cancelled.disconnect(self._on_op_finished)
907                 self._session.draft.error.disconnect(self._on_op_error)
908                 self._voicemailPlayer.destroy()
909                 window = self._window
910                 self._window = None
911                 try:
912                         window.close()
913                         window.destroy()
914                 except AttributeError:
915                         _moduleLogger.exception("Oh well")
916                 except RuntimeError:
917                         _moduleLogger.exception("Oh well")
918
919         def update_orientation(self, orientation):
920                 qwrappers.WindowWrapper.update_orientation(self, orientation)
921                 self._scroll_to_bottom()
922
923         def _update_letter_count(self):
924                 count = len(self._smsEntry.toPlainText())
925                 numTexts, numCharInText = divmod(count, self.MAX_CHAR)
926                 numTexts += 1
927                 numCharsLeftInText = self.MAX_CHAR - numCharInText
928                 self._characterCountLabel.setText("%d (%d)" % (numCharsLeftInText, numTexts))
929
930         def _update_button_state(self):
931                 self._cancelButton.setEnabled(True)
932                 if self._session.draft.get_num_contacts() == 0:
933                         self._dialButton.setEnabled(False)
934                         self._smsButton.setEnabled(False)
935                 elif self._session.draft.get_num_contacts() == 1:
936                         count = len(self._smsEntry.toPlainText())
937                         if count == 0:
938                                 self._dialButton.setEnabled(True)
939                                 self._smsButton.setEnabled(False)
940                         else:
941                                 self._dialButton.setEnabled(False)
942                                 self._smsButton.setEnabled(True)
943                 else:
944                         self._dialButton.setEnabled(False)
945                         count = len(self._smsEntry.toPlainText())
946                         if count == 0:
947                                 self._smsButton.setEnabled(False)
948                         else:
949                                 self._smsButton.setEnabled(True)
950
951         def _update_history(self, cid):
952                 draftContactsCount = self._session.draft.get_num_contacts()
953                 if draftContactsCount != 1:
954                         self._history.setVisible(False)
955                 else:
956                         description = self._session.draft.get_description(cid)
957
958                         self._targetList.setVisible(False)
959                         if description:
960                                 self._history.setText(description)
961                                 self._history.setVisible(True)
962                         else:
963                                 self._history.setText("")
964                                 self._history.setVisible(False)
965
966         def _update_target_fields(self):
967                 draftContactsCount = self._session.draft.get_num_contacts()
968                 if draftContactsCount == 0:
969                         self.hide()
970                         del self._cids[:]
971                 elif draftContactsCount == 1:
972                         (cid, ) = self._session.draft.get_contacts()
973                         title = self._session.draft.get_title(cid)
974                         numbers = self._session.draft.get_numbers(cid)
975
976                         self._targetList.setVisible(False)
977                         self._update_history(cid)
978                         self._populate_number_selector(self._singleNumberSelector, cid, 0, numbers)
979                         self._cids = [cid]
980
981                         self._scroll_to_bottom()
982                         self._window.setWindowTitle(title)
983                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
984                         self.show()
985                         self._window.raise_()
986                 else:
987                         self._targetList.setVisible(True)
988                         self._targetList.update()
989                         self._history.setText("")
990                         self._history.setVisible(False)
991                         self._singleNumberSelector.setVisible(False)
992
993                         self._scroll_to_bottom()
994                         self._window.setWindowTitle("Contacts")
995                         self._smsEntry.setFocus(QtCore.Qt.OtherFocusReason)
996                         self.show()
997                         self._window.raise_()
998
999         def _populate_number_selector(self, selector, cid, cidIndex, numbers):
1000                 selector.clear()
1001
1002                 selectedNumber = self._session.draft.get_selected_number(cid)
1003                 if len(numbers) == 1:
1004                         # If no alt numbers available, check the address book
1005                         numbers, defaultIndex = _get_contact_numbers(self._session, cid, selectedNumber, numbers[0][1])
1006                 else:
1007                         defaultIndex = _index_number(numbers, selectedNumber)
1008
1009                 for number, description in numbers:
1010                         if description:
1011                                 label = "%s - %s" % (number, description)
1012                         else:
1013                                 label = number
1014                         selector.addItem(label)
1015                 selector.setVisible(True)
1016                 if 1 < len(numbers):
1017                         selector.setEnabled(True)
1018                         selector.setCurrentIndex(defaultIndex)
1019                 else:
1020                         selector.setEnabled(False)
1021
1022         def _scroll_to_bottom(self):
1023                 self._scrollTimer.start()
1024
1025         @misc_utils.log_exception(_moduleLogger)
1026         def _on_delayed_scroll_to_bottom(self):
1027                 with qui_utils.notify_error(self._app.errorLog):
1028                         self._scrollEntry.ensureWidgetVisible(self._smsEntry)
1029
1030         @misc_utils.log_exception(_moduleLogger)
1031         def _on_sms_clicked(self, arg):
1032                 with qui_utils.notify_error(self._app.errorLog):
1033                         message = unicode(self._smsEntry.toPlainText())
1034                         self._session.draft.message = message
1035                         self._session.draft.send()
1036
1037         @misc_utils.log_exception(_moduleLogger)
1038         def _on_call_clicked(self, arg):
1039                 with qui_utils.notify_error(self._app.errorLog):
1040                         message = unicode(self._smsEntry.toPlainText())
1041                         self._session.draft.message = message
1042                         self._session.draft.call()
1043
1044         @qt_compat.Slot()
1045         @misc_utils.log_exception(_moduleLogger)
1046         def _on_cancel_clicked(self, message):
1047                 with qui_utils.notify_error(self._app.errorLog):
1048                         self._session.draft.cancel()
1049
1050         @misc_utils.log_exception(_moduleLogger)
1051         def _on_single_change_number(self, index):
1052                 with qui_utils.notify_error(self._app.errorLog):
1053                         # Exception thrown when the first item is removed
1054                         cid = self._cids[0]
1055                         try:
1056                                 numbers = self._session.draft.get_numbers(cid)
1057                         except KeyError:
1058                                 _moduleLogger.error("Contact no longer available (or bizarre error): %r (%r)" % (cid, index))
1059                                 return
1060                         number = numbers[index][0]
1061                         self._session.draft.set_selected_number(cid, number)
1062
1063         @qt_compat.Slot()
1064         @misc_utils.log_exception(_moduleLogger)
1065         def _on_refresh_history(self):
1066                 with qui_utils.notify_error(self._app.errorLog):
1067                         draftContactsCount = self._session.draft.get_num_contacts()
1068                         if draftContactsCount != 1:
1069                                 # Changing contact count will automatically refresh it
1070                                 return
1071                         (cid, ) = self._session.draft.get_contacts()
1072                         self._update_history(cid)
1073
1074         @qt_compat.Slot()
1075         @misc_utils.log_exception(_moduleLogger)
1076         def _on_recipients_changed(self):
1077                 with qui_utils.notify_error(self._app.errorLog):
1078                         self._update_target_fields()
1079                         self._update_button_state()
1080
1081         @qt_compat.Slot()
1082         @misc_utils.log_exception(_moduleLogger)
1083         def _on_op_started(self):
1084                 with qui_utils.notify_error(self._app.errorLog):
1085                         self._smsEntry.setReadOnly(True)
1086                         self._smsButton.setVisible(False)
1087                         self._dialButton.setVisible(False)
1088                         self.show()
1089
1090         @qt_compat.Slot()
1091         @misc_utils.log_exception(_moduleLogger)
1092         def _on_calling_started(self):
1093                 with qui_utils.notify_error(self._app.errorLog):
1094                         self._cancelButton.setVisible(True)
1095
1096         @qt_compat.Slot()
1097         @misc_utils.log_exception(_moduleLogger)
1098         def _on_op_finished(self):
1099                 with qui_utils.notify_error(self._app.errorLog):
1100                         self._smsEntry.setPlainText("")
1101                         self._smsEntry.setReadOnly(False)
1102                         self._cancelButton.setVisible(False)
1103                         self._smsButton.setVisible(True)
1104                         self._dialButton.setVisible(True)
1105                         self.close()
1106                         self.destroy()
1107
1108         @qt_compat.Slot()
1109         @misc_utils.log_exception(_moduleLogger)
1110         def _on_op_error(self, message):
1111                 with qui_utils.notify_error(self._app.errorLog):
1112                         self._smsEntry.setReadOnly(False)
1113                         self._cancelButton.setVisible(False)
1114                         self._smsButton.setVisible(True)
1115                         self._dialButton.setVisible(True)
1116
1117                         self._errorLog.push_error(message)
1118
1119         @qt_compat.Slot()
1120         @misc_utils.log_exception(_moduleLogger)
1121         def _on_letter_count_changed(self):
1122                 with qui_utils.notify_error(self._app.errorLog):
1123                         self._update_letter_count()
1124                         self._update_button_state()
1125
1126         @qt_compat.Slot()
1127         @misc_utils.log_exception(_moduleLogger)
1128         def _on_window_resized(self):
1129                 with qui_utils.notify_error(self._app.errorLog):
1130                         self._scroll_to_bottom()
1131
1132         @qt_compat.Slot()
1133         @qt_compat.Slot(bool)
1134         @misc_utils.log_exception(_moduleLogger)
1135         def _on_close_window(self, checked = True):
1136                 with qui_utils.notify_error(self._app.errorLog):
1137                         self.close()
1138
1139
1140 def _index_number(numbers, default):
1141         uglyDefault = misc_utils.make_ugly(default)
1142         uglyContactNumbers = list(
1143                 misc_utils.make_ugly(contactNumber)
1144                 for (contactNumber, _) in numbers
1145         )
1146         defaultMatches = [
1147                 misc_utils.similar_ugly_numbers(uglyDefault, contactNumber)
1148                 for contactNumber in uglyContactNumbers
1149         ]
1150         try:
1151                 defaultIndex = defaultMatches.index(True)
1152         except ValueError:
1153                 defaultIndex = -1
1154                 _moduleLogger.warn(
1155                         "Could not find contact number %s among %r" % (
1156                                 default, numbers
1157                         )
1158                 )
1159         return defaultIndex
1160
1161
1162 def _get_contact_numbers(session, contactId, number, description):
1163         contactPhoneNumbers = []
1164         if contactId and contactId != "0":
1165                 try:
1166                         contactDetails = copy.deepcopy(session.get_contacts()[contactId])
1167                         contactPhoneNumbers = contactDetails["numbers"]
1168                 except KeyError:
1169                         contactPhoneNumbers = []
1170                 contactPhoneNumbers = [
1171                         (contactPhoneNumber["phoneNumber"], contactPhoneNumber.get("phoneType", "Unknown"))
1172                         for contactPhoneNumber in contactPhoneNumbers
1173                 ]
1174                 defaultIndex = _index_number(contactPhoneNumbers, number)
1175
1176         if not contactPhoneNumbers or defaultIndex == -1:
1177                 contactPhoneNumbers += [(number, description)]
1178                 defaultIndex = 0
1179
1180         return contactPhoneNumbers, defaultIndex