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