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