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