Limiting the led being turned off to only when refreshing the recent or messages...
[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 Figure out how to integrate with the Maemo contacts app
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                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
74                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
75                 '/usr/lib/dialcentral/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         def __init__(self):
90                 self._initDone = False
91                 self._connection = None
92                 self._osso = None
93                 self._clipboard = gtk.clipboard_get()
94
95                 self._credentials = ("", "")
96                 self._selectedBackendId = self.NULL_BACKEND
97                 self._defaultBackendId = self.GC_BACKEND
98                 self._phoneBackends = None
99                 self._dialpads = None
100                 self._accountViews = None
101                 self._messagesViews = None
102                 self._recentViews = None
103                 self._contactsViews = None
104                 self._alarmHandler = None
105                 self._ledHandler = None
106                 self._originalCurrentLabels = []
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 under hildon, rely on the application name being shown
154                 if hildon is None:
155                         self._window.set_title("%s" % constants.__pretty_app_name__)
156
157                 callbackMapping = {
158                         "on_dialpad_quit": self._on_close,
159                 }
160                 self._widgetTree.signal_autoconnect(callbackMapping)
161
162                 self._window.connect("destroy", self._on_close)
163                 self._window.set_default_size(800, 300)
164                 self._window.show_all()
165
166                 self._loginSink = gtk_toolbox.threaded_stage(
167                         gtk_toolbox.comap(
168                                 self.attempt_login,
169                                 gtk_toolbox.null_sink(),
170                         )
171                 )
172
173                 backgroundSetup = threading.Thread(target=self._idle_setup)
174                 backgroundSetup.setDaemon(True)
175                 backgroundSetup.start()
176
177         def _idle_setup(self):
178                 """
179                 If something can be done after the UI loads, push it here so it's not blocking the UI
180                 """
181                 try:
182                         # Barebones UI handlers
183                         import null_backend
184                         import null_views
185
186                         self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
187                         with gtk_toolbox.gtk_lock():
188                                 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
189                                 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
190                                 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
191                                 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
192                                 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
193
194                                 self._dialpads[self._selectedBackendId].enable()
195                                 self._accountViews[self._selectedBackendId].enable()
196                                 self._recentViews[self._selectedBackendId].enable()
197                                 self._messagesViews[self._selectedBackendId].enable()
198                                 self._contactsViews[self._selectedBackendId].enable()
199
200                         # Setup maemo specifics
201                         try:
202                                 import osso
203                         except ImportError:
204                                 osso = None
205                         self._osso = None
206                         if osso is not None:
207                                 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
208                                 device = osso.DeviceState(self._osso)
209                                 device.set_device_state_callback(self._on_device_state_change, 0)
210                         else:
211                                 pass # warnings.warn("No OSSO", UserWarning, 2)
212
213                         try:
214                                 import alarm_handler
215                                 self._alarmHandler = alarm_handler.AlarmHandler()
216                         except ImportError:
217                                 alarm_handler = None
218                         if hildon is not None:
219                                 import led_handler
220                                 self._ledHandler = led_handler.LedHandler()
221
222                         # Setup maemo specifics
223                         try:
224                                 import conic
225                         except ImportError:
226                                 conic = None
227                         self._connection = None
228                         if conic is not None:
229                                 self._connection = conic.Connection()
230                                 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
231                                 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
232                         else:
233                                 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
234
235                         # Setup costly backends
236                         import gv_backend
237                         import gc_backend
238                         import file_backend
239                         import evo_backend
240                         import gc_views
241
242                         try:
243                                 os.makedirs(constants._data_path_)
244                         except OSError, e:
245                                 if e.errno != 17:
246                                         raise
247                         gcCookiePath = os.path.join(constants._data_path_, "gc_cookies.txt")
248                         gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
249                         self._defaultBackendId = self._guess_preferred_backend((
250                                 (self.GC_BACKEND, gcCookiePath),
251                                 (self.GV_BACKEND, gvCookiePath),
252                         ))
253
254                         self._phoneBackends.update({
255                                 self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
256                                 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
257                         })
258                         with gtk_toolbox.gtk_lock():
259                                 unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
260                                 unifiedDialpad.set_number("")
261                                 self._dialpads.update({
262                                         self.GC_BACKEND: unifiedDialpad,
263                                         self.GV_BACKEND: unifiedDialpad,
264                                 })
265                                 self._accountViews.update({
266                                         self.GC_BACKEND: gc_views.AccountInfo(
267                                                 self._widgetTree, self._phoneBackends[self.GC_BACKEND], None, self._errorDisplay
268                                         ),
269                                         self.GV_BACKEND: gc_views.AccountInfo(
270                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
271                                         ),
272                                 })
273                                 self._accountViews[self.GC_BACKEND].save_everything = lambda *args: None
274                                 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
275                                 self._recentViews.update({
276                                         self.GC_BACKEND: gc_views.RecentCallsView(
277                                                 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
278                                         ),
279                                         self.GV_BACKEND: gc_views.RecentCallsView(
280                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
281                                         ),
282                                 })
283                                 self._messagesViews.update({
284                                         self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
285                                         self.GV_BACKEND: gc_views.MessagesView(
286                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
287                                         ),
288                                 })
289                                 self._contactsViews.update({
290                                         self.GC_BACKEND: gc_views.ContactsView(
291                                                 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
292                                         ),
293                                         self.GV_BACKEND: gc_views.ContactsView(
294                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
295                                         ),
296                                 })
297
298                         evoBackend = evo_backend.EvolutionAddressBook()
299                         fsContactsPath = os.path.join(constants._data_path_, "contacts")
300                         fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
301                         for backendId in (self.GV_BACKEND, self.GC_BACKEND):
302                                 self._dialpads[backendId].number_selected = self._select_action
303                                 self._recentViews[backendId].number_selected = self._select_action
304                                 self._messagesViews[backendId].number_selected = self._select_action
305                                 self._contactsViews[backendId].number_selected = self._select_action
306
307                                 addressBooks = [
308                                         self._phoneBackends[backendId],
309                                         evoBackend,
310                                         fileBackend,
311                                 ]
312                                 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
313                                 self._contactsViews[backendId].append(mergedBook)
314                                 self._contactsViews[backendId].extend(addressBooks)
315                                 self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
316
317                         callbackMapping = {
318                                 "on_paste": self._on_paste,
319                                 "on_refresh": self._on_menu_refresh,
320                                 "on_clearcookies_clicked": self._on_clearcookies_clicked,
321                                 "on_notebook_switch_page": self._on_notebook_switch_page,
322                                 "on_about_activate": self._on_about_activate,
323                         }
324                         self._widgetTree.signal_autoconnect(callbackMapping)
325
326                         with gtk_toolbox.gtk_lock():
327                                 self._originalCurrentLabels = [
328                                         self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
329                                         for pageIndex in xrange(self._notebook.get_n_pages())
330                                 ]
331                                 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
332                                 self._notebookTapHandler.enable()
333                         self._notebookTapHandler.on_tap = self._reset_tab_refresh
334                         self._notebookTapHandler.on_hold = self._on_tab_refresh
335                         self._notebookTapHandler.on_holding = self._set_tab_refresh
336                         self._notebookTapHandler.on_cancel = self._reset_tab_refresh
337
338                         self._initDone = True
339
340                         config = ConfigParser.SafeConfigParser()
341                         config.read(constants._user_settings_)
342                         with gtk_toolbox.gtk_lock():
343                                 self.load_settings(config)
344
345                         self._spawn_attempt_login(2)
346                 except Exception, e:
347                         with gtk_toolbox.gtk_lock():
348                                 self._errorDisplay.push_exception()
349
350         def attempt_login(self, numOfAttempts = 10, force = False):
351                 """
352                 @todo Handle user notification better like attempting to login and failed login
353
354                 @note This must be run outside of the UI lock
355                 """
356                 try:
357                         assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
358                         assert self._initDone, "Attempting login before app is fully loaded"
359
360                         serviceId = self.NULL_BACKEND
361                         loggedIn = False
362                         if not force:
363                                 try:
364                                         self.refresh_session()
365                                         serviceId = self._defaultBackendId
366                                         loggedIn = True
367                                 except StandardError, e:
368                                         warnings.warn('Session refresh failed with the following message "%s"' % e.message, UserWarning, 2)
369
370                         if not loggedIn:
371                                 loggedIn, serviceId = self._login_by_user(numOfAttempts)
372
373                         with gtk_toolbox.gtk_lock():
374                                 self._change_loggedin_status(serviceId)
375                 except StandardError, e:
376                         with gtk_toolbox.gtk_lock():
377                                 self._errorDisplay.push_exception()
378
379         def _spawn_attempt_login(self, *args):
380                 self._loginSink.send(args)
381
382         def refresh_session(self):
383                 """
384                 @note Thread agnostic
385                 """
386                 assert self._initDone, "Attempting login before app is fully loaded"
387
388                 loggedIn = False
389                 if not loggedIn:
390                         loggedIn = self._login_by_cookie()
391                 if not loggedIn:
392                         loggedIn = self._login_by_settings()
393
394                 if not loggedIn:
395                         raise RuntimeError("Login Failed")
396
397         def _login_by_cookie(self):
398                 """
399                 @note Thread agnostic
400                 """
401                 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
402                 if loggedIn:
403                         warnings.warn(
404                                 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
405                                 UserWarning, 2
406                         )
407                 return loggedIn
408
409         def _login_by_settings(self):
410                 """
411                 @note Thread agnostic
412                 """
413                 username, password = self._credentials
414                 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
415                 if loggedIn:
416                         self._credentials = username, password
417                         warnings.warn(
418                                 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
419                                 UserWarning, 2
420                         )
421                 return loggedIn
422
423         def _login_by_user(self, numOfAttempts):
424                 """
425                 @note This must be run outside of the UI lock
426                 """
427                 loggedIn, (username, password) = False, self._credentials
428                 tmpServiceId = self.NULL_BACKEND
429                 for attemptCount in xrange(numOfAttempts):
430                         if loggedIn:
431                                 break
432                         availableServices = (
433                                 (self.GV_BACKEND, "Google Voice"),
434                                 (self.GC_BACKEND, "Grand Central"),
435                         )
436                         with gtk_toolbox.gtk_lock():
437                                 credentials = self._credentialsDialog.request_credentials_from(
438                                         availableServices, defaultCredentials = self._credentials
439                                 )
440                         tmpServiceId, username, password = credentials
441                         loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
442
443                 if loggedIn:
444                         serviceId = tmpServiceId
445                         self._credentials = username, password
446                         warnings.warn(
447                                 "Logged into %r through user request" % self._phoneBackends[serviceId],
448                                 UserWarning, 2
449                         )
450                 else:
451                         serviceId = self.NULL_BACKEND
452
453                 return loggedIn, serviceId
454
455         def _select_action(self, action, number, message):
456                 self.refresh_session()
457                 if action == "select":
458                         self._dialpads[self._selectedBackendId].set_number(number)
459                         self._notebook.set_current_page(self.KEYPAD_TAB)
460                 elif action == "dial":
461                         self._on_dial_clicked(number)
462                 elif action == "sms":
463                         self._on_sms_clicked(number, message)
464                 else:
465                         assert False, "Unknown action: %s" % action
466
467         def _change_loggedin_status(self, newStatus):
468                 oldStatus = self._selectedBackendId
469                 if oldStatus == newStatus:
470                         return
471
472                 self._dialpads[oldStatus].disable()
473                 self._accountViews[oldStatus].disable()
474                 self._recentViews[oldStatus].disable()
475                 self._messagesViews[oldStatus].disable()
476                 self._contactsViews[oldStatus].disable()
477
478                 self._dialpads[newStatus].enable()
479                 self._accountViews[newStatus].enable()
480                 self._recentViews[newStatus].enable()
481                 self._messagesViews[newStatus].enable()
482                 self._contactsViews[newStatus].enable()
483
484                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
485                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
486                 self._accountViews[self._selectedBackendId].update()
487
488                 self._selectedBackendId = newStatus
489
490         def load_settings(self, config):
491                 """
492                 @note UI Thread
493                 """
494                 try:
495                         self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
496                         blobs = (
497                                 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
498                                 for i in xrange(len(self._credentials))
499                         )
500                         creds = (
501                                 base64.b64decode(blob)
502                                 for blob in blobs
503                         )
504                         self._credentials = tuple(creds)
505
506                         if self._alarmHandler is not None:
507                                 self._alarmHandler.load_settings(config, "alarm")
508                 except ConfigParser.NoOptionError, e:
509                         warnings.warn(
510                                 "Settings file %s is missing section %s" % (
511                                         constants._user_settings_,
512                                         e.section,
513                                 ),
514                                 stacklevel=2
515                         )
516                 except ConfigParser.NoSectionError, e:
517                         warnings.warn(
518                                 "Settings file %s is missing section %s" % (
519                                         constants._user_settings_,
520                                         e.section,
521                                 ),
522                                 stacklevel=2
523                         )
524
525                 for backendId, view in itertools.chain(
526                         self._dialpads.iteritems(),
527                         self._accountViews.iteritems(),
528                         self._messagesViews.iteritems(),
529                         self._recentViews.iteritems(),
530                         self._contactsViews.iteritems(),
531                 ):
532                         sectionName = "%s - %s" % (backendId, view.name())
533                         try:
534                                 view.load_settings(config, sectionName)
535                         except ConfigParser.NoOptionError, e:
536                                 warnings.warn(
537                                         "Settings file %s is missing section %s" % (
538                                                 constants._user_settings_,
539                                                 e.section,
540                                         ),
541                                         stacklevel=2
542                                 )
543                         except ConfigParser.NoSectionError, e:
544                                 warnings.warn(
545                                         "Settings file %s is missing section %s" % (
546                                                 constants._user_settings_,
547                                                 e.section,
548                                         ),
549                                         stacklevel=2
550                                 )
551
552         def save_settings(self, config):
553                 """
554                 @note Thread Agnostic
555                 """
556                 config.add_section(constants.__pretty_app_name__)
557                 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
558                 for i, value in enumerate(self._credentials):
559                         blob = base64.b64encode(value)
560                         config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
561                 config.add_section("alarm")
562                 if self._alarmHandler is not None:
563                         self._alarmHandler.save_settings(config, "alarm")
564
565                 for backendId, view in itertools.chain(
566                         self._dialpads.iteritems(),
567                         self._accountViews.iteritems(),
568                         self._messagesViews.iteritems(),
569                         self._recentViews.iteritems(),
570                         self._contactsViews.iteritems(),
571                 ):
572                         sectionName = "%s - %s" % (backendId, view.name())
573                         config.add_section(sectionName)
574                         view.save_settings(config, sectionName)
575
576         def _guess_preferred_backend(self, backendAndCookiePaths):
577                 modTimeAndPath = [
578                         (getmtime_nothrow(path), backendId, path)
579                         for backendId, path in backendAndCookiePaths
580                 ]
581                 modTimeAndPath.sort()
582                 return modTimeAndPath[-1][1]
583
584         def _save_settings(self):
585                 """
586                 @note Thread Agnostic
587                 """
588                 config = ConfigParser.SafeConfigParser()
589                 self.save_settings(config)
590                 with open(constants._user_settings_, "wb") as configFile:
591                         config.write(configFile)
592
593         def _refresh_active_tab(self):
594                 pageIndex = self._notebook.get_current_page()
595                 if pageIndex == self.CONTACTS_TAB:
596                         self._contactsViews[self._selectedBackendId].update(force=True)
597                 elif pageIndex == self.RECENT_TAB:
598                         self._recentViews[self._selectedBackendId].update(force=True)
599                 elif pageIndex == self.MESSAGES_TAB:
600                         self._messagesViews[self._selectedBackendId].update(force=True)
601
602                 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
603                         if self._ledHandler is not None:
604                                 self._ledHandler.off()
605
606         def _on_close(self, *args, **kwds):
607                 try:
608                         if self._osso is not None:
609                                 self._osso.close()
610
611                         if self._initDone:
612                                 self._save_settings()
613                 finally:
614                         gtk.main_quit()
615
616         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
617                 """
618                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
619                 For system_inactivity, we have no background tasks to pause
620
621                 @note Hildon specific
622                 """
623                 if memory_low:
624                         for backendId in self.BACKENDS:
625                                 self._phoneBackends[backendId].clear_caches()
626                         self._contactsViews[self._selectedBackendId].clear_caches()
627                         gc.collect()
628
629                 if save_unsaved_data or shutdown:
630                         self._save_settings()
631
632         def _on_connection_change(self, connection, event, magicIdentifier):
633                 """
634                 @note Hildon specific
635                 """
636                 import conic
637
638                 status = event.get_status()
639                 error = event.get_error()
640                 iap_id = event.get_iap_id()
641                 bearer = event.get_bearer_type()
642
643                 if status == conic.STATUS_CONNECTED:
644                         if self._initDone:
645                                 self._spawn_attempt_login(2)
646                 elif status == conic.STATUS_DISCONNECTED:
647                         if self._initDone:
648                                 self._defaultBackendId = self._selectedBackendId
649                                 self._change_loggedin_status(self.NULL_BACKEND)
650
651         def _on_window_state_change(self, widget, event, *args):
652                 """
653                 @note Hildon specific
654                 """
655                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
656                         self._isFullScreen = True
657                 else:
658                         self._isFullScreen = False
659
660         def _on_key_press(self, widget, event, *args):
661                 """
662                 @note Hildon specific
663                 """
664                 if event.keyval == gtk.keysyms.F6:
665                         if self._isFullScreen:
666                                 self._window.unfullscreen()
667                         else:
668                                 self._window.fullscreen()
669
670         def _on_clearcookies_clicked(self, *args):
671                 self._phoneBackends[self._selectedBackendId].logout()
672                 self._accountViews[self._selectedBackendId].clear()
673                 self._recentViews[self._selectedBackendId].clear()
674                 self._messagesViews[self._selectedBackendId].clear()
675                 self._contactsViews[self._selectedBackendId].clear()
676                 self._change_loggedin_status(self.NULL_BACKEND)
677
678                 self._spawn_attempt_login(2, True)
679
680         def _on_notebook_switch_page(self, notebook, page, pageIndex):
681                 self._reset_tab_refresh()
682                 if pageIndex == self.RECENT_TAB:
683                         self._recentViews[self._selectedBackendId].update()
684                 elif pageIndex == self.MESSAGES_TAB:
685                         self._messagesViews[self._selectedBackendId].update()
686                 elif pageIndex == self.CONTACTS_TAB:
687                         self._contactsViews[self._selectedBackendId].update()
688                 elif pageIndex == self.ACCOUNT_TAB:
689                         self._accountViews[self._selectedBackendId].update()
690
691         def _set_tab_refresh(self, *args):
692                 pageIndex = self._notebook.get_current_page()
693                 child = self._notebook.get_nth_page(pageIndex)
694                 self._notebook.get_tab_label(child).set_text("Refresh?")
695                 return False
696
697         def _reset_tab_refresh(self, *args):
698                 pageIndex = self._notebook.get_current_page()
699                 child = self._notebook.get_nth_page(pageIndex)
700                 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
701                 return False
702
703         def _on_tab_refresh(self, *args):
704                 self._refresh_active_tab()
705                 self._reset_tab_refresh()
706                 return False
707
708         def _on_sms_clicked(self, number, message):
709                 assert number, "No number specified"
710                 assert message, "Empty message"
711                 try:
712                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
713                 except StandardError, e:
714                         loggedIn = False
715                         self._errorDisplay.push_exception()
716                         return
717
718                 if not loggedIn:
719                         self._errorDisplay.push_message(
720                                 "Backend link with grandcentral is not working, please try again"
721                         )
722                         return
723
724                 dialed = False
725                 try:
726                         self._phoneBackends[self._selectedBackendId].send_sms(number, message)
727                         dialed = True
728                 except StandardError, e:
729                         self._errorDisplay.push_exception()
730                 except ValueError, e:
731                         self._errorDisplay.push_exception()
732
733         def _on_dial_clicked(self, number):
734                 assert number, "No number to call"
735                 try:
736                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
737                 except StandardError, e:
738                         loggedIn = False
739                         self._errorDisplay.push_exception()
740                         return
741
742                 if not loggedIn:
743                         self._errorDisplay.push_message(
744                                 "Backend link with grandcentral is not working, please try again"
745                         )
746                         return
747
748                 dialed = False
749                 try:
750                         assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
751                         self._phoneBackends[self._selectedBackendId].dial(number)
752                         dialed = True
753                 except StandardError, e:
754                         self._errorDisplay.push_exception()
755                 except ValueError, e:
756                         self._errorDisplay.push_exception()
757
758                 if dialed:
759                         self._dialpads[self._selectedBackendId].clear()
760
761         def _on_menu_refresh(self, *args):
762                 self._refresh_active_tab()
763
764         def _on_paste(self, *args):
765                 contents = self._clipboard.wait_for_text()
766                 self._dialpads[self._selectedBackendId].set_number(contents)
767
768         def _on_about_activate(self, *args):
769                 dlg = gtk.AboutDialog()
770                 dlg.set_name(constants.__pretty_app_name__)
771                 dlg.set_version(constants.__version__)
772                 dlg.set_copyright("Copyright 2008 - LGPL")
773                 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")
774                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
775                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
776                 dlg.run()
777                 dlg.destroy()
778
779
780 def run_doctest():
781         import doctest
782
783         failureCount, testCount = doctest.testmod()
784         if not failureCount:
785                 print "Tests Successful"
786                 sys.exit(0)
787         else:
788                 sys.exit(1)
789
790
791 def run_dialpad():
792         _lock_file = os.path.join(constants._data_path_, ".lock")
793         with gtk_toolbox.flock(_lock_file, 0):
794                 gtk.gdk.threads_init()
795
796                 if hildon is not None:
797                         gtk.set_application_name(constants.__pretty_app_name__)
798                 handle = Dialcentral()
799                 gtk.main()
800
801
802 class DummyOptions(object):
803
804         def __init__(self):
805                 self.test = False
806
807
808 if __name__ == "__main__":
809         if len(sys.argv) > 1:
810                 try:
811                         import optparse
812                 except ImportError:
813                         optparse = None
814
815                 if optparse is not None:
816                         parser = optparse.OptionParser()
817                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
818                         (commandOptions, commandArgs) = parser.parse_args()
819         else:
820                 commandOptions = DummyOptions()
821                 commandArgs = []
822
823         if commandOptions.test:
824                 run_doctest()
825         else:
826                 run_dialpad()