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