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