Version bump to 1.0, description fixes, and adding of names to the recent tab for...
[gc-dialer] / src / dc_glade.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 @todo Look into an actor system
22 @bug Session timeouts are bad, possible solutions:
23         @li For every X minutes, if logged in, attempt login
24         @li Restructure so code handling login/dial/sms is beneath everything else and login attempts are made if those fail
25 @todo Can't text from dialpad (so can't do any arbitrary number texts)
26 @todo Add logging support to make debugging issues for people a lot easier
27 """
28
29
30 from __future__ import with_statement
31
32 import sys
33 import gc
34 import os
35 import threading
36 import base64
37 import ConfigParser
38 import itertools
39 import warnings
40
41 import gtk
42 import gtk.glade
43
44 try:
45         import hildon
46 except ImportError:
47         hildon = None
48
49 import constants
50 import gtk_toolbox
51
52
53 def getmtime_nothrow(path):
54         try:
55                 return os.path.getmtime(path)
56         except StandardError:
57                 return 0
58
59
60 def display_error_message(msg):
61         error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
62
63         def close(dialog, response):
64                 dialog.destroy()
65         error_dialog.connect("response", close)
66         error_dialog.run()
67
68
69 class Dialcentral(object):
70
71         _glade_files = [
72                 '/usr/lib/dialcentral/dialcentral.glade',
73                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
74                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
75         ]
76
77         KEYPAD_TAB = 0
78         RECENT_TAB = 1
79         MESSAGES_TAB = 2
80         CONTACTS_TAB = 3
81         ACCOUNT_TAB = 4
82
83         NULL_BACKEND = 0
84         GC_BACKEND = 1
85         GV_BACKEND = 2
86         BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
87
88         _data_path = os.path.join(os.path.expanduser("~"), ".dialcentral")
89         _user_settings = "%s/settings.ini" % _data_path
90
91         def __init__(self):
92                 self._initDone = False
93                 self._connection = None
94                 self._osso = None
95                 self._clipboard = gtk.clipboard_get()
96
97                 self._deviceIsOnline = True
98                 self._credentials = ("", "")
99                 self._selectedBackendId = self.NULL_BACKEND
100                 self._defaultBackendId = self.GC_BACKEND
101                 self._phoneBackends = None
102                 self._dialpads = None
103                 self._accountViews = None
104                 self._messagesViews = None
105                 self._recentViews = None
106                 self._contactsViews = None
107
108                 for path in self._glade_files:
109                         if os.path.isfile(path):
110                                 self._widgetTree = gtk.glade.XML(path)
111                                 break
112                 else:
113                         display_error_message("Cannot find dialcentral.glade")
114                         gtk.main_quit()
115                         return
116
117                 self._window = self._widgetTree.get_widget("mainWindow")
118                 self._notebook = self._widgetTree.get_widget("notebook")
119                 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
120                 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
121
122                 self._app = None
123                 self._isFullScreen = False
124                 if hildon is not None:
125                         self._app = hildon.Program()
126                         oldWindow = self._window
127                         self._window = hildon.Window()
128                         oldWindow.get_child().reparent(self._window)
129                         self._app.add_window(self._window)
130
131                         try:
132                                 self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
133                                 self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
134                                 self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
135                         except TypeError, e:
136                                 warnings.warn(e.message)
137                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
138                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('message_scrolledwindow'), True)
139                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
140
141                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
142                         menu = gtk.Menu()
143                         for child in gtkMenu.get_children():
144                                 child.reparent(menu)
145                         self._window.set_menu(menu)
146                         gtkMenu.destroy()
147
148                         self._window.connect("key-press-event", self._on_key_press)
149                         self._window.connect("window-state-event", self._on_window_state_change)
150                 else:
151                         pass # warnings.warn("No Hildon", UserWarning, 2)
152
153                 if hildon is not None:
154                         self._window.set_title("Keypad")
155                 else:
156                         self._window.set_title("%s - Keypad" % constants.__pretty_app_name__)
157
158                 callbackMapping = {
159                         "on_dialpad_quit": self._on_close,
160                 }
161                 self._widgetTree.signal_autoconnect(callbackMapping)
162
163                 self._window.connect("destroy", self._on_close)
164                 self._window.set_default_size(800, 300)
165                 self._window.show_all()
166
167                 backgroundSetup = threading.Thread(target=self._idle_setup)
168                 backgroundSetup.setDaemon(True)
169                 backgroundSetup.start()
170
171         def _idle_setup(self):
172                 """
173                 If something can be done after the UI loads, push it here so it's not blocking the UI
174                 """
175                 try:
176                         # Barebones UI handlers
177                         import null_backend
178                         import null_views
179
180                         self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
181                         with gtk_toolbox.gtk_lock():
182                                 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
183                                 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
184                                 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
185                                 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
186                                 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
187
188                                 self._dialpads[self._selectedBackendId].enable()
189                                 self._accountViews[self._selectedBackendId].enable()
190                                 self._recentViews[self._selectedBackendId].enable()
191                                 self._messagesViews[self._selectedBackendId].enable()
192                                 self._contactsViews[self._selectedBackendId].enable()
193
194                         # Setup maemo specifics
195                         try:
196                                 import osso
197                         except ImportError:
198                                 osso = None
199                         self._osso = None
200                         if osso is not None:
201                                 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
202                                 device = osso.DeviceState(self._osso)
203                                 device.set_device_state_callback(self._on_device_state_change, 0)
204                         else:
205                                 pass # warnings.warn("No OSSO", UserWarning, 2)
206
207                         # Setup maemo specifics
208                         try:
209                                 import conic
210                         except ImportError:
211                                 conic = None
212                         self._connection = None
213                         if conic is not None:
214                                 self._connection = conic.Connection()
215                                 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
216                                 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
217                         else:
218                                 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
219
220                         # Setup costly backends
221                         import gv_backend
222                         import gc_backend
223                         import file_backend
224                         import evo_backend
225                         import gc_views
226
227                         try:
228                                 os.makedirs(self._data_path)
229                         except OSError, e:
230                                 if e.errno != 17:
231                                         raise
232                         gcCookiePath = os.path.join(self._data_path, "gc_cookies.txt")
233                         gvCookiePath = os.path.join(self._data_path, "gv_cookies.txt")
234                         self._defaultBackendId = self._guess_preferred_backend((
235                                 (self.GC_BACKEND, gcCookiePath),
236                                 (self.GV_BACKEND, gvCookiePath),
237                         ))
238
239                         self._phoneBackends.update({
240                                 self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
241                                 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
242                         })
243                         with gtk_toolbox.gtk_lock():
244                                 unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
245                                 unifiedDialpad.set_number("")
246                                 self._dialpads.update({
247                                         self.GC_BACKEND: unifiedDialpad,
248                                         self.GV_BACKEND: unifiedDialpad,
249                                 })
250                                 self._accountViews.update({
251                                         self.GC_BACKEND: gc_views.AccountInfo(
252                                                 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
253                                         ),
254                                         self.GV_BACKEND: gc_views.AccountInfo(
255                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
256                                         ),
257                                 })
258                                 self._recentViews.update({
259                                         self.GC_BACKEND: gc_views.RecentCallsView(
260                                                 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
261                                         ),
262                                         self.GV_BACKEND: gc_views.RecentCallsView(
263                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
264                                         ),
265                                 })
266                                 self._messagesViews.update({
267                                         self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
268                                         self.GV_BACKEND: gc_views.MessagesView(
269                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
270                                         ),
271                                 })
272                                 self._contactsViews.update({
273                                         self.GC_BACKEND: gc_views.ContactsView(
274                                                 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
275                                         ),
276                                         self.GV_BACKEND: gc_views.ContactsView(
277                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
278                                         ),
279                                 })
280
281                         evoBackend = evo_backend.EvolutionAddressBook()
282                         fsContactsPath = os.path.join(self._data_path, "contacts")
283                         fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
284                         for backendId in (self.GV_BACKEND, self.GC_BACKEND):
285                                 self._dialpads[backendId].number_selected = self._select_action
286                                 self._recentViews[backendId].number_selected = self._select_action
287                                 self._messagesViews[backendId].number_selected = self._select_action
288                                 self._contactsViews[backendId].number_selected = self._select_action
289
290                                 addressBooks = [
291                                         self._phoneBackends[backendId],
292                                         evoBackend,
293                                         fileBackend,
294                                 ]
295                                 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
296                                 self._contactsViews[backendId].append(mergedBook)
297                                 self._contactsViews[backendId].extend(addressBooks)
298                                 self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
299
300                         callbackMapping = {
301                                 "on_paste": self._on_paste,
302                                 "on_refresh": self._on_refresh,
303                                 "on_clearcookies_clicked": self._on_clearcookies_clicked,
304                                 "on_notebook_switch_page": self._on_notebook_switch_page,
305                                 "on_about_activate": self._on_about_activate,
306                         }
307                         self._widgetTree.signal_autoconnect(callbackMapping)
308
309                         self._initDone = True
310
311                         config = ConfigParser.SafeConfigParser()
312                         config.read(self._user_settings)
313                         with gtk_toolbox.gtk_lock():
314                                 self.load_settings(config)
315
316                         gtk_toolbox.asynchronous_gtk_message(self._spawn_attempt_login)(2)
317                 except StandardError, e:
318                         warnings.warn(e.message, UserWarning, 2)
319                 except BaseException, e:
320                         try:
321                                 warnings.warn(e.message, UserWarning, 2)
322                         finally:
323                                 raise
324
325         def attempt_login(self, numOfAttempts = 10, force = False):
326                 """
327                 @todo Handle user notification better like attempting to login and failed login
328
329                 @note This must be run outside of the UI lock
330                 """
331                 try:
332                         assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
333                         assert self._initDone, "Attempting login before app is fully loaded"
334                         if not self._deviceIsOnline:
335                                 raise RuntimeError("Unable to login, device is not online")
336
337                         serviceId = self.NULL_BACKEND
338                         loggedIn = False
339                         if not force:
340                                 try:
341                                         self.refresh_session()
342                                         serviceId = self._defaultBackendId
343                                         loggedIn = True
344                                 except StandardError, e:
345                                         warnings.warn('Session refresh failed with the following message "%s"' % e.message, UserWarning, 2)
346
347                         if not loggedIn:
348                                 loggedIn, serviceId = self._login_by_user(numOfAttempts)
349
350                         with gtk_toolbox.gtk_lock():
351                                 self._change_loggedin_status(serviceId)
352                 except StandardError, e:
353                         with gtk_toolbox.gtk_lock():
354                                 self._errorDisplay.push_exception(e)
355
356         def _spawn_attempt_login(self, *args):
357                 backgroundLogin = threading.Thread(target=self.attempt_login, args=args)
358                 backgroundLogin.setDaemon(True)
359                 backgroundLogin.start()
360
361         def refresh_session(self):
362                 """
363                 @note Thread agnostic
364                 """
365                 assert self._initDone, "Attempting login before app is fully loaded"
366                 if not self._deviceIsOnline:
367                         raise RuntimeError("Unable to login, device is not online")
368
369                 loggedIn = False
370                 if not loggedIn:
371                         loggedIn = self._login_by_cookie()
372                 if not loggedIn:
373                         loggedIn = self._login_by_settings()
374
375                 if not loggedIn:
376                         raise RuntimeError("Login Failed")
377
378         def _login_by_cookie(self):
379                 """
380                 @note Thread agnostic
381                 """
382                 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
383                 if loggedIn:
384                         warnings.warn(
385                                 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
386                                 UserWarning, 2
387                         )
388                 return loggedIn
389
390         def _login_by_settings(self):
391                 """
392                 @note Thread agnostic
393                 """
394                 username, password = self._credentials
395                 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
396                 if loggedIn:
397                         self._credentials = username, password
398                         warnings.warn(
399                                 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
400                                 UserWarning, 2
401                         )
402                 return loggedIn
403
404         def _login_by_user(self, numOfAttempts):
405                 """
406                 @note This must be run outside of the UI lock
407                 """
408                 loggedIn, (username, password) = False, self._credentials
409                 tmpServiceId = self.NULL_BACKEND
410                 for attemptCount in xrange(numOfAttempts):
411                         if loggedIn:
412                                 break
413                         availableServices = {
414                                 self.GV_BACKEND: "Google Voice",
415                                 self.GC_BACKEND: "Grand Central",
416                         }
417                         with gtk_toolbox.gtk_lock():
418                                 credentials = self._credentialsDialog.request_credentials_from(
419                                         availableServices, defaultCredentials = self._credentials
420                                 )
421                         tmpServiceId, username, password = credentials
422                         loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
423
424                 if loggedIn:
425                         serviceId = tmpServiceId
426                         self._credentials = username, password
427                         warnings.warn(
428                                 "Logged into %r through user request" % self._phoneBackends[serviceId],
429                                 UserWarning, 2
430                         )
431                 else:
432                         serviceId = self.NULL_BACKEND
433
434                 return loggedIn, serviceId
435
436         def _select_action(self, action, number, message):
437                 self.refresh_session()
438                 if action == "select":
439                         self._dialpads[self._selectedBackendId].set_number(number)
440                         self._notebook.set_current_page(self.KEYPAD_TAB)
441                 elif action == "dial":
442                         self._on_dial_clicked(number)
443                 elif action == "sms":
444                         self._on_sms_clicked(number, message)
445                 else:
446                         assert False, "Unknown action: %s" % action
447
448         def _change_loggedin_status(self, newStatus):
449                 oldStatus = self._selectedBackendId
450                 if oldStatus == newStatus:
451                         return
452
453                 self._dialpads[oldStatus].disable()
454                 self._accountViews[oldStatus].disable()
455                 self._recentViews[oldStatus].disable()
456                 self._messagesViews[oldStatus].disable()
457                 self._contactsViews[oldStatus].disable()
458
459                 self._dialpads[newStatus].enable()
460                 self._accountViews[newStatus].enable()
461                 self._recentViews[newStatus].enable()
462                 self._messagesViews[newStatus].enable()
463                 self._contactsViews[newStatus].enable()
464
465                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
466                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
467                 self._accountViews[self._selectedBackendId].update()
468
469                 self._selectedBackendId = newStatus
470
471         def load_settings(self, config):
472                 """
473                 @note UI Thread
474                 """
475                 try:
476                         self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
477                         blobs = (
478                                 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
479                                 for i in xrange(len(self._credentials))
480                         )
481                         creds = (
482                                 base64.b64decode(blob)
483                                 for blob in blobs
484                         )
485                         self._credentials = tuple(creds)
486                 except ConfigParser.NoSectionError, e:
487                         warnings.warn(
488                                 "Settings file %s is missing section %s" % (
489                                         self._user_settings,
490                                         e.section,
491                                 ),
492                                 stacklevel=2
493                         )
494
495                 for backendId, view in itertools.chain(
496                         self._dialpads.iteritems(),
497                         self._accountViews.iteritems(),
498                         self._messagesViews.iteritems(),
499                         self._recentViews.iteritems(),
500                         self._contactsViews.iteritems(),
501                 ):
502                         sectionName = "%s - %s" % (backendId, view.name())
503                         try:
504                                 view.load_settings(config, sectionName)
505                         except ConfigParser.NoSectionError, e:
506                                 warnings.warn(
507                                         "Settings file %s is missing section %s" % (
508                                                 self._user_settings,
509                                                 e.section,
510                                         ),
511                                         stacklevel=2
512                                 )
513
514         def save_settings(self, config):
515                 """
516                 @note Thread Agnostic
517                 """
518                 config.add_section(constants.__pretty_app_name__)
519                 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
520                 for i, value in enumerate(self._credentials):
521                         blob = base64.b64encode(value)
522                         config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
523                 for backendId, view in itertools.chain(
524                         self._dialpads.iteritems(),
525                         self._accountViews.iteritems(),
526                         self._messagesViews.iteritems(),
527                         self._recentViews.iteritems(),
528                         self._contactsViews.iteritems(),
529                 ):
530                         sectionName = "%s - %s" % (backendId, view.name())
531                         config.add_section(sectionName)
532                         view.save_settings(config, sectionName)
533
534         def _guess_preferred_backend(self, backendAndCookiePaths):
535                 modTimeAndPath = [
536                         (getmtime_nothrow(path), backendId, path)
537                         for backendId, path in backendAndCookiePaths
538                 ]
539                 modTimeAndPath.sort()
540                 return modTimeAndPath[-1][1]
541
542         def _save_settings(self):
543                 """
544                 @note Thread Agnostic
545                 """
546                 config = ConfigParser.SafeConfigParser()
547                 self.save_settings(config)
548                 with open(self._user_settings, "wb") as configFile:
549                         config.write(configFile)
550
551         def _on_close(self, *args, **kwds):
552                 try:
553                         if self._osso is not None:
554                                 self._osso.close()
555
556                         if self._initDone:
557                                 self._save_settings()
558                 finally:
559                         gtk.main_quit()
560
561         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
562                 """
563                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
564                 For system_inactivity, we have no background tasks to pause
565
566                 @note Hildon specific
567                 """
568                 if memory_low:
569                         for backendId in self.BACKENDS:
570                                 self._phoneBackends[backendId].clear_caches()
571                         self._contactsViews[self._selectedBackendId].clear_caches()
572                         gc.collect()
573
574                 if save_unsaved_data or shutdown:
575                         self._save_settings()
576
577         def _on_connection_change(self, connection, event, magicIdentifier):
578                 """
579                 @note Hildon specific
580                 """
581                 import conic
582
583                 status = event.get_status()
584                 error = event.get_error()
585                 iap_id = event.get_iap_id()
586                 bearer = event.get_bearer_type()
587
588                 if status == conic.STATUS_CONNECTED:
589                         self._deviceIsOnline = True
590                         if self._initDone:
591                                 self._spawn_attempt_login(2)
592                 elif status == conic.STATUS_DISCONNECTED:
593                         self._deviceIsOnline = False
594                         if self._initDone:
595                                 self._defaultBackendId = self._selectedBackendId
596                                 self._change_loggedin_status(self.NULL_BACKEND)
597
598         def _on_window_state_change(self, widget, event, *args):
599                 """
600                 @note Hildon specific
601                 """
602                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
603                         self._isFullScreen = True
604                 else:
605                         self._isFullScreen = False
606
607         def _on_key_press(self, widget, event, *args):
608                 """
609                 @note Hildon specific
610                 """
611                 if event.keyval == gtk.keysyms.F6:
612                         if self._isFullScreen:
613                                 self._window.unfullscreen()
614                         else:
615                                 self._window.fullscreen()
616
617         def _on_clearcookies_clicked(self, *args):
618                 self._phoneBackends[self._selectedBackendId].logout()
619                 self._accountViews[self._selectedBackendId].clear()
620                 self._recentViews[self._selectedBackendId].clear()
621                 self._messagesViews[self._selectedBackendId].clear()
622                 self._contactsViews[self._selectedBackendId].clear()
623                 self._change_loggedin_status(self.NULL_BACKEND)
624
625                 self._spawn_attempt_login(2, True)
626
627         def _on_notebook_switch_page(self, notebook, page, page_num):
628                 if page_num == self.RECENT_TAB:
629                         self._recentViews[self._selectedBackendId].update()
630                 elif page_num == self.MESSAGES_TAB:
631                         self._messagesViews[self._selectedBackendId].update()
632                 elif page_num == self.CONTACTS_TAB:
633                         self._contactsViews[self._selectedBackendId].update()
634                 elif page_num == self.ACCOUNT_TAB:
635                         self._accountViews[self._selectedBackendId].update()
636
637                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
638                 if hildon is not None:
639                         self._window.set_title(tabTitle)
640                 else:
641                         self._window.set_title("%s - %s" % (constants.__pretty_app_name__, tabTitle))
642
643         def _on_sms_clicked(self, number, message):
644                 assert number
645                 assert message
646                 try:
647                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
648                 except RuntimeError, e:
649                         loggedIn = False
650                         self._errorDisplay.push_exception(e)
651                         return
652
653                 if not loggedIn:
654                         self._errorDisplay.push_message(
655                                 "Backend link with grandcentral is not working, please try again"
656                         )
657                         return
658
659                 dialed = False
660                 try:
661                         self._phoneBackends[self._selectedBackendId].send_sms(number, message)
662                         dialed = True
663                 except RuntimeError, e:
664                         self._errorDisplay.push_exception(e)
665                 except ValueError, e:
666                         self._errorDisplay.push_exception(e)
667
668         def _on_dial_clicked(self, number):
669                 assert number
670                 try:
671                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
672                 except RuntimeError, e:
673                         loggedIn = False
674                         self._errorDisplay.push_exception(e)
675                         return
676
677                 if not loggedIn:
678                         self._errorDisplay.push_message(
679                                 "Backend link with grandcentral is not working, please try again"
680                         )
681                         return
682
683                 dialed = False
684                 try:
685                         assert self._phoneBackends[self._selectedBackendId].get_callback_number() != ""
686                         self._phoneBackends[self._selectedBackendId].dial(number)
687                         dialed = True
688                 except RuntimeError, e:
689                         self._errorDisplay.push_exception(e)
690                 except ValueError, e:
691                         self._errorDisplay.push_exception(e)
692
693                 if dialed:
694                         self._dialpads[self._selectedBackendId].clear()
695
696         def _on_refresh(self, *args):
697                 page_num = self._notebook.get_current_page()
698                 if page_num == self.CONTACTS_TAB:
699                         self._contactsViews[self._selectedBackendId].update(force=True)
700                 elif page_num == self.RECENT_TAB:
701                         self._recentViews[self._selectedBackendId].update(force=True)
702                 elif page_num == self.MESSAGES_TAB:
703                         self._messagesViews[self._selectedBackendId].update(force=True)
704
705         def _on_paste(self, *args):
706                 contents = self._clipboard.wait_for_text()
707                 self._dialpads[self._selectedBackendId].set_number(contents)
708
709         def _on_about_activate(self, *args):
710                 dlg = gtk.AboutDialog()
711                 dlg.set_name(constants.__pretty_app_name__)
712                 dlg.set_version(constants.__version__)
713                 dlg.set_copyright("Copyright 2008 - LGPL")
714                 dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice/Grandcentral account.  This application is not affiliated with Google in any way")
715                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
716                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
717                 dlg.run()
718                 dlg.destroy()
719
720
721 def run_doctest():
722         import doctest
723
724         failureCount, testCount = doctest.testmod()
725         if not failureCount:
726                 print "Tests Successful"
727                 sys.exit(0)
728         else:
729                 sys.exit(1)
730
731
732 def run_dialpad():
733         gtk.gdk.threads_init()
734         if hildon is not None:
735                 gtk.set_application_name(constants.__pretty_app_name__)
736         handle = Dialcentral()
737         gtk.main()
738
739
740 class DummyOptions(object):
741
742         def __init__(self):
743                 self.test = False
744
745
746 if __name__ == "__main__":
747         if len(sys.argv) > 1:
748                 try:
749                         import optparse
750                 except ImportError:
751                         optparse = None
752
753                 if optparse is not None:
754                         parser = optparse.OptionParser()
755                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
756                         (commandOptions, commandArgs) = parser.parse_args()
757         else:
758                 commandOptions = DummyOptions()
759                 commandArgs = []
760
761         if commandOptions.test:
762                 run_doctest()
763         else:
764                 run_dialpad()