Cutting down the number of times the login dialog is needlessly displayed
[gc-dialer] / src / dc_glade.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's GoogleVoice 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
22
23 from __future__ import with_statement
24
25 import sys
26 import gc
27 import os
28 import threading
29 import base64
30 import ConfigParser
31 import itertools
32 import logging
33
34 import gtk
35 import gtk.glade
36
37 import constants
38 import hildonize
39 import gtk_toolbox
40
41
42 _moduleLogger = logging.getLogger("dc_glade")
43
44
45 def getmtime_nothrow(path):
46         try:
47                 return os.path.getmtime(path)
48         except Exception:
49                 return 0
50
51
52 def display_error_message(msg):
53         error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
54
55         def close(dialog, response):
56                 dialog.destroy()
57         error_dialog.connect("response", close)
58         error_dialog.run()
59
60
61 class Dialcentral(object):
62
63         _glade_files = [
64                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
65                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
66                 '/usr/lib/dialcentral/dialcentral.glade',
67         ]
68
69         KEYPAD_TAB = 0
70         RECENT_TAB = 1
71         MESSAGES_TAB = 2
72         CONTACTS_TAB = 3
73         ACCOUNT_TAB = 4
74
75         NULL_BACKEND = 0
76         # 1 Was GrandCentral support so the gap was maintained for compatibility
77         GV_BACKEND = 2
78         BACKENDS = (NULL_BACKEND, GV_BACKEND)
79
80         def __init__(self):
81                 self._initDone = False
82                 self._connection = None
83                 self._osso = None
84                 self._clipboard = gtk.clipboard_get()
85
86                 self._credentials = ("", "")
87                 self._selectedBackendId = self.NULL_BACKEND
88                 self._defaultBackendId = self.GV_BACKEND
89                 self._phoneBackends = None
90                 self._dialpads = None
91                 self._accountViews = None
92                 self._messagesViews = None
93                 self._recentViews = None
94                 self._contactsViews = None
95                 self._alarmHandler = None
96                 self._ledHandler = None
97                 self._originalCurrentLabels = []
98
99                 for path in self._glade_files:
100                         if os.path.isfile(path):
101                                 self._widgetTree = gtk.glade.XML(path)
102                                 break
103                 else:
104                         display_error_message("Cannot find dialcentral.glade")
105                         gtk.main_quit()
106                         return
107
108                 self._window = self._widgetTree.get_widget("mainWindow")
109                 self._notebook = self._widgetTree.get_widget("notebook")
110                 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
111                 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
112
113                 self._isFullScreen = False
114                 self._app = hildonize.get_app_class()()
115                 self._window = hildonize.hildonize_window(self._app, self._window)
116                 hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
117                 hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
118
119                 for scrollingWidgetName in (
120                         'recent_scrolledwindow',
121                         'message_scrolledwindow',
122                         'contacts_scrolledwindow',
123                         "smsMessages_scrolledwindow",
124                 ):
125                         scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
126                         assert scrollingWidget is not None, scrollingWidgetName
127                         hildonize.hildonize_scrollwindow(scrollingWidget)
128                 for scrollingWidgetName in (
129                         "smsMessage_scrolledEntry",
130                 ):
131                         scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
132                         assert scrollingWidget is not None, scrollingWidgetName
133                         hildonize.hildonize_scrollwindow_with_viewport(scrollingWidget)
134
135                 for buttonName in (
136                         "back",
137                         "addressbookSelectButton",
138                         "sendSmsButton",
139                         "dialButton",
140                         "cancelSmsButton",
141                         "callbackSelectButton",
142                         "minutesEntryButton",
143                         "clearcookies",
144                         "phoneTypeSelection",
145                 ):
146                         button = self._widgetTree.get_widget(buttonName)
147                         assert button is not None, buttonName
148                         hildonize.set_button_thumb_selectable(button)
149
150                 replacementButtons = [gtk.Button("Test")]
151                 menu = hildonize.hildonize_menu(
152                         self._window,
153                         self._widgetTree.get_widget("dialpad_menubar"),
154                         replacementButtons
155                 )
156
157                 self._window.connect("key-press-event", self._on_key_press)
158                 self._window.connect("window-state-event", self._on_window_state_change)
159                 if not hildonize.IS_HILDON_SUPPORTED:
160                         _moduleLogger.warning("No hildonization support")
161
162                 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
163
164                 self._window.connect("destroy", self._on_close)
165                 self._window.set_default_size(800, 300)
166                 self._window.show_all()
167
168                 self._loginSink = gtk_toolbox.threaded_stage(
169                         gtk_toolbox.comap(
170                                 self._attempt_login,
171                                 gtk_toolbox.null_sink(),
172                         )
173                 )
174
175                 backgroundSetup = threading.Thread(target=self._idle_setup)
176                 backgroundSetup.setDaemon(True)
177                 backgroundSetup.start()
178
179         def _idle_setup(self):
180                 """
181                 If something can be done after the UI loads, push it here so it's not blocking the UI
182                 """
183                 # Barebones UI handlers
184                 try:
185                         import null_backend
186                         import null_views
187
188                         self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
189                         with gtk_toolbox.gtk_lock():
190                                 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
191                                 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
192                                 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
193                                 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
194                                 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
195
196                                 self._dialpads[self._selectedBackendId].enable()
197                                 self._accountViews[self._selectedBackendId].enable()
198                                 self._recentViews[self._selectedBackendId].enable()
199                                 self._messagesViews[self._selectedBackendId].enable()
200                                 self._contactsViews[self._selectedBackendId].enable()
201                 except Exception, e:
202                         with gtk_toolbox.gtk_lock():
203                                 self._errorDisplay.push_exception()
204
205                 # Setup maemo specifics
206                 try:
207                         try:
208                                 import osso
209                         except (ImportError, OSError):
210                                 osso = None
211                         self._osso = None
212                         if osso is not None:
213                                 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
214                                 device = osso.DeviceState(self._osso)
215                                 device.set_device_state_callback(self._on_device_state_change, 0)
216                         else:
217                                 _moduleLogger.warning("No device state support")
218
219                         try:
220                                 import alarm_handler
221                                 self._alarmHandler = alarm_handler.AlarmHandler()
222                         except (ImportError, OSError):
223                                 alarm_handler = None
224                         except Exception:
225                                 with gtk_toolbox.gtk_lock():
226                                         self._errorDisplay.push_exception()
227                                 alarm_handler = None
228                                 _moduleLogger.warning("No notification support")
229                         if hildonize.IS_HILDON_SUPPORTED:
230                                 try:
231                                         import led_handler
232                                         self._ledHandler = led_handler.LedHandler()
233                                 except Exception, e:
234                                         _moduleLogger.exception('LED Handling failed: "%s"' % str(e))
235                                         self._ledHandler = None
236                         else:
237                                 self._ledHandler = None
238
239                         try:
240                                 import conic
241                         except (ImportError, OSError):
242                                 conic = None
243                         self._connection = None
244                         if conic is not None:
245                                 self._connection = conic.Connection()
246                                 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
247                                 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
248                         else:
249                                 _moduleLogger.warning("No connection support")
250                 except Exception, e:
251                         with gtk_toolbox.gtk_lock():
252                                 self._errorDisplay.push_exception()
253
254                 # Setup costly backends
255                 try:
256                         import gv_backend
257                         import file_backend
258                         import gv_views
259
260                         try:
261                                 os.makedirs(constants._data_path_)
262                         except OSError, e:
263                                 if e.errno != 17:
264                                         raise
265                         gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
266
267                         self._phoneBackends.update({
268                                 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
269                         })
270                         with gtk_toolbox.gtk_lock():
271                                 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
272                                 self._dialpads.update({
273                                         self.GV_BACKEND: unifiedDialpad,
274                                 })
275                                 self._accountViews.update({
276                                         self.GV_BACKEND: gv_views.AccountInfo(
277                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
278                                         ),
279                                 })
280                                 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
281                                 self._recentViews.update({
282                                         self.GV_BACKEND: gv_views.RecentCallsView(
283                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
284                                         ),
285                                 })
286                                 self._messagesViews.update({
287                                         self.GV_BACKEND: gv_views.MessagesView(
288                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
289                                         ),
290                                 })
291                                 self._contactsViews.update({
292                                         self.GV_BACKEND: gv_views.ContactsView(
293                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
294                                         ),
295                                 })
296
297                         fsContactsPath = os.path.join(constants._data_path_, "contacts")
298                         fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
299
300                         self._dialpads[self.GV_BACKEND].number_selected = self._select_action
301                         self._recentViews[self.GV_BACKEND].number_selected = self._select_action
302                         self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
303                         self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
304
305                         addressBooks = [
306                                 self._phoneBackends[self.GV_BACKEND],
307                                 fileBackend,
308                         ]
309                         mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
310                         self._contactsViews[self.GV_BACKEND].append(mergedBook)
311                         self._contactsViews[self.GV_BACKEND].extend(addressBooks)
312                         self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
313
314                         callbackMapping = {
315                                 "on_paste": self._on_paste,
316                                 "on_refresh": self._on_menu_refresh,
317                                 "on_clearcookies_clicked": self._on_clearcookies_clicked,
318                                 "on_about_activate": self._on_about_activate,
319                         }
320                         if hildonize.GTK_MENU_USED:
321                                 self._widgetTree.signal_autoconnect(callbackMapping)
322                         self._notebook.connect("switch-page", self._on_notebook_switch_page)
323                         self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked)
324
325                         with gtk_toolbox.gtk_lock():
326                                 self._originalCurrentLabels = [
327                                         self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
328                                         for pageIndex in xrange(self._notebook.get_n_pages())
329                                 ]
330                                 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
331                                 self._notebookTapHandler.enable()
332                         self._notebookTapHandler.on_tap = self._reset_tab_refresh
333                         self._notebookTapHandler.on_hold = self._on_tab_refresh
334                         self._notebookTapHandler.on_holding = self._set_tab_refresh
335                         self._notebookTapHandler.on_cancel = self._reset_tab_refresh
336
337                         self._initDone = True
338
339                         config = ConfigParser.SafeConfigParser()
340                         config.read(constants._user_settings_)
341                         with gtk_toolbox.gtk_lock():
342                                 self.load_settings(config)
343                 except Exception, e:
344                         with gtk_toolbox.gtk_lock():
345                                 self._errorDisplay.push_exception()
346                 finally:
347                         self._spawn_attempt_login()
348
349         def _spawn_attempt_login(self, *args):
350                 self._loginSink.send(args)
351
352         def _attempt_login(self, force = False):
353                 """
354                 @note This must be run outside of the UI lock
355                 """
356                 try:
357                         assert self._initDone, "Attempting login before app is fully loaded"
358
359                         serviceId = self.NULL_BACKEND
360                         loggedIn = False
361                         if not force:
362                                 with gtk_toolbox.gtk_lock():
363                                         banner = hildonize.show_busy_banner_start(self._window, "Logging In...")
364                                 try:
365                                         self.refresh_session()
366                                         serviceId = self._defaultBackendId
367                                         loggedIn = True
368                                 except Exception, e:
369                                         _moduleLogger.exception('Session refresh failed with the following message "%s"' % str(e))
370                                 finally:
371                                         with gtk_toolbox.gtk_lock():
372                                                 hildonize.show_busy_banner_end(banner)
373
374                         if not loggedIn:
375                                 loggedIn, serviceId = self._login_by_user()
376
377                         with gtk_toolbox.gtk_lock():
378                                 self._change_loggedin_status(serviceId)
379                                 if loggedIn:
380                                         hildonize.show_information_banner(self._window, "Logged In")
381                                 else:
382                                         hildonize.show_information_banner(self._window, "Login Failed")
383                 except Exception, e:
384                         with gtk_toolbox.gtk_lock():
385                                 self._errorDisplay.push_exception()
386
387         def refresh_session(self):
388                 """
389                 @note Thread agnostic
390                 """
391                 assert self._initDone, "Attempting login before app is fully loaded"
392
393                 loggedIn = False
394                 if not loggedIn:
395                         loggedIn = self._login_by_cookie()
396                 if not loggedIn:
397                         loggedIn = self._login_by_settings()
398
399                 if not loggedIn:
400                         raise RuntimeError("Login Failed")
401
402         def _login_by_cookie(self):
403                 """
404                 @note Thread agnostic
405                 """
406                 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
407                 if loggedIn:
408                         _moduleLogger.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
409                 return loggedIn
410
411         def _login_by_settings(self):
412                 """
413                 @note Thread agnostic
414                 """
415                 username, password = self._credentials
416                 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
417                 if loggedIn:
418                         self._credentials = username, password
419                         _moduleLogger.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
420                 return loggedIn
421
422         def _login_by_user(self):
423                 """
424                 @note This must be run outside of the UI lock
425                 """
426                 loggedIn, (username, password) = False, self._credentials
427                 tmpServiceId = self.GV_BACKEND
428                 while not loggedIn:
429                         with gtk_toolbox.gtk_lock():
430                                 credentials = self._credentialsDialog.request_credentials(
431                                         defaultCredentials = self._credentials
432                                 )
433                                 if not self._phoneBackends[tmpServiceId].get_callback_number():
434                                         # subtle reminder to the users to configure things
435                                         self._notebook.set_current_page(self.ACCOUNT_TAB)
436
437                                 banner = hildonize.show_busy_banner_start(self._window, "Logging In...")
438                         try:
439                                 username, password = credentials
440                                 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
441                         finally:
442                                 with gtk_toolbox.gtk_lock():
443                                         hildonize.show_busy_banner_end(banner)
444
445                 if loggedIn:
446                         serviceId = tmpServiceId
447                         self._credentials = username, password
448                         _moduleLogger.info("Logged into %r through user request" % self._phoneBackends[serviceId])
449                 else:
450                         # Hint to the user that they are not logged in
451                         serviceId = self.NULL_BACKEND
452                         self._notebook.set_current_page(self.ACCOUNT_TAB)
453
454                 return loggedIn, serviceId
455
456         def _select_action(self, action, number, message):
457                 self.refresh_session()
458                 if action == "dial":
459                         self._on_dial_clicked(number)
460                 elif action == "sms":
461                         self._on_sms_clicked(number, message)
462                 else:
463                         assert False, "Unknown action: %s" % action
464
465         def _change_loggedin_status(self, newStatus):
466                 oldStatus = self._selectedBackendId
467                 if oldStatus == newStatus:
468                         return
469
470                 self._dialpads[oldStatus].disable()
471                 self._accountViews[oldStatus].disable()
472                 self._recentViews[oldStatus].disable()
473                 self._messagesViews[oldStatus].disable()
474                 self._contactsViews[oldStatus].disable()
475
476                 self._dialpads[newStatus].enable()
477                 self._accountViews[newStatus].enable()
478                 self._recentViews[newStatus].enable()
479                 self._messagesViews[newStatus].enable()
480                 self._contactsViews[newStatus].enable()
481
482                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
483                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
484
485                 self._selectedBackendId = newStatus
486
487                 self._accountViews[self._selectedBackendId].update()
488                 self._refresh_active_tab()
489
490         def load_settings(self, config):
491                 """
492                 @note UI Thread
493                 """
494                 try:
495                         self._defaultBackendId = config.getint(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                         _moduleLogger.exception(
510                                 "Settings file %s is missing section %s" % (
511                                         constants._user_settings_,
512                                         e.section,
513                                 ),
514                         )
515                 except ConfigParser.NoSectionError, e:
516                         _moduleLogger.exception(
517                                 "Settings file %s is missing section %s" % (
518                                         constants._user_settings_,
519                                         e.section,
520                                 ),
521                         )
522
523                 for backendId, view in itertools.chain(
524                         self._dialpads.iteritems(),
525                         self._accountViews.iteritems(),
526                         self._messagesViews.iteritems(),
527                         self._recentViews.iteritems(),
528                         self._contactsViews.iteritems(),
529                 ):
530                         sectionName = "%s - %s" % (backendId, view.name())
531                         try:
532                                 view.load_settings(config, sectionName)
533                         except ConfigParser.NoOptionError, e:
534                                 _moduleLogger.exception(
535                                         "Settings file %s is missing section %s" % (
536                                                 constants._user_settings_,
537                                                 e.section,
538                                         ),
539                                 )
540                         except ConfigParser.NoSectionError, e:
541                                 _moduleLogger.exception(
542                                         "Settings file %s is missing section %s" % (
543                                                 constants._user_settings_,
544                                                 e.section,
545                                         ),
546                                 )
547
548                 try:
549                         previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
550                         if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
551                                 hildonize.window_to_landscape(self._window)
552                         elif previousOrientation == gtk.ORIENTATION_VERTICAL:
553                                 hildonize.window_to_portrait(self._window)
554                 except ConfigParser.NoOptionError, e:
555                         _moduleLogger.exception(
556                                 "Settings file %s is missing section %s" % (
557                                         constants._user_settings_,
558                                         e.section,
559                                 ),
560                         )
561                 except ConfigParser.NoSectionError, e:
562                         _moduleLogger.exception(
563                                 "Settings file %s is missing section %s" % (
564                                         constants._user_settings_,
565                                         e.section,
566                                 ),
567                         )
568
569         def save_settings(self, config):
570                 """
571                 @note Thread Agnostic
572                 """
573                 # Because we now only support GVoice, if there are user credentials,
574                 # always assume its using the GVoice backend
575                 if self._credentials[0] and self._credentials[1]:
576                         backend = self.GV_BACKEND
577                 else:
578                         backend = self.NULL_BACKEND
579
580                 config.add_section(constants.__pretty_app_name__)
581                 config.set(constants.__pretty_app_name__, "active", str(backend))
582                 config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
583                 for i, value in enumerate(self._credentials):
584                         blob = base64.b64encode(value)
585                         config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
586                 config.add_section("alarm")
587                 if self._alarmHandler is not None:
588                         self._alarmHandler.save_settings(config, "alarm")
589
590                 for backendId, view in itertools.chain(
591                         self._dialpads.iteritems(),
592                         self._accountViews.iteritems(),
593                         self._messagesViews.iteritems(),
594                         self._recentViews.iteritems(),
595                         self._contactsViews.iteritems(),
596                 ):
597                         sectionName = "%s - %s" % (backendId, view.name())
598                         config.add_section(sectionName)
599                         view.save_settings(config, sectionName)
600
601         def _save_settings(self):
602                 """
603                 @note Thread Agnostic
604                 """
605                 config = ConfigParser.SafeConfigParser()
606                 self.save_settings(config)
607                 with open(constants._user_settings_, "wb") as configFile:
608                         config.write(configFile)
609
610         def _refresh_active_tab(self):
611                 pageIndex = self._notebook.get_current_page()
612                 if pageIndex == self.CONTACTS_TAB:
613                         self._contactsViews[self._selectedBackendId].update(force=True)
614                 elif pageIndex == self.RECENT_TAB:
615                         self._recentViews[self._selectedBackendId].update(force=True)
616                 elif pageIndex == self.MESSAGES_TAB:
617                         self._messagesViews[self._selectedBackendId].update(force=True)
618
619                 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
620                         if self._ledHandler is not None:
621                                 self._ledHandler.off()
622
623         def _on_close(self, *args, **kwds):
624                 try:
625                         if self._osso is not None:
626                                 self._osso.close()
627
628                         if self._initDone:
629                                 self._save_settings()
630                 finally:
631                         gtk.main_quit()
632
633         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
634                 """
635                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
636                 For system_inactivity, we have no background tasks to pause
637
638                 @note Hildon specific
639                 """
640                 try:
641                         if memory_low:
642                                 for backendId in self.BACKENDS:
643                                         self._phoneBackends[backendId].clear_caches()
644                                 self._contactsViews[self._selectedBackendId].clear_caches()
645                                 gc.collect()
646
647                         if save_unsaved_data or shutdown:
648                                 self._save_settings()
649                 except Exception, e:
650                         self._errorDisplay.push_exception()
651
652         def _on_connection_change(self, connection, event, magicIdentifier):
653                 """
654                 @note Hildon specific
655                 """
656                 try:
657                         import conic
658
659                         status = event.get_status()
660                         error = event.get_error()
661                         iap_id = event.get_iap_id()
662                         bearer = event.get_bearer_type()
663
664                         if status == conic.STATUS_CONNECTED:
665                                 if self._initDone:
666                                         self._spawn_attempt_login()
667                         elif status == conic.STATUS_DISCONNECTED:
668                                 if self._initDone:
669                                         self._defaultBackendId = self._selectedBackendId
670                                         self._change_loggedin_status(self.NULL_BACKEND)
671                 except Exception, e:
672                         self._errorDisplay.push_exception()
673
674         def _on_window_state_change(self, widget, event, *args):
675                 """
676                 @note Hildon specific
677                 """
678                 try:
679                         if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
680                                 self._isFullScreen = True
681                         else:
682                                 self._isFullScreen = False
683                 except Exception, e:
684                         self._errorDisplay.push_exception()
685
686         def _on_key_press(self, widget, event, *args):
687                 """
688                 @note Hildon specific
689                 """
690                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
691                 try:
692                         if (
693                                 event.keyval == gtk.keysyms.F6 or
694                                 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
695                         ):
696                                 if self._isFullScreen:
697                                         self._window.unfullscreen()
698                                 else:
699                                         self._window.fullscreen()
700                 except Exception, e:
701                         self._errorDisplay.push_exception()
702
703         def _on_clearcookies_clicked(self, *args):
704                 try:
705                         self._phoneBackends[self._selectedBackendId].logout()
706                         self._accountViews[self._selectedBackendId].clear()
707                         self._recentViews[self._selectedBackendId].clear()
708                         self._messagesViews[self._selectedBackendId].clear()
709                         self._contactsViews[self._selectedBackendId].clear()
710                         self._change_loggedin_status(self.NULL_BACKEND)
711
712                         self._spawn_attempt_login(True)
713                 except Exception, e:
714                         self._errorDisplay.push_exception()
715
716         def _on_notebook_switch_page(self, notebook, page, pageIndex):
717                 try:
718                         self._reset_tab_refresh()
719
720                         didRecentUpdate = False
721                         didMessagesUpdate = False
722
723                         if pageIndex == self.RECENT_TAB:
724                                 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
725                         elif pageIndex == self.MESSAGES_TAB:
726                                 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
727                         elif pageIndex == self.CONTACTS_TAB:
728                                 self._contactsViews[self._selectedBackendId].update()
729                         elif pageIndex == self.ACCOUNT_TAB:
730                                 self._accountViews[self._selectedBackendId].update()
731
732                         if didRecentUpdate or didMessagesUpdate:
733                                 if self._ledHandler is not None:
734                                         self._ledHandler.off()
735                 except Exception, e:
736                         self._errorDisplay.push_exception()
737
738         def _set_tab_refresh(self, *args):
739                 try:
740                         pageIndex = self._notebook.get_current_page()
741                         child = self._notebook.get_nth_page(pageIndex)
742                         self._notebook.get_tab_label(child).set_text("Refresh?")
743                 except Exception, e:
744                         self._errorDisplay.push_exception()
745                 return False
746
747         def _reset_tab_refresh(self, *args):
748                 try:
749                         pageIndex = self._notebook.get_current_page()
750                         child = self._notebook.get_nth_page(pageIndex)
751                         self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
752                 except Exception, e:
753                         self._errorDisplay.push_exception()
754                 return False
755
756         def _on_tab_refresh(self, *args):
757                 try:
758                         self._refresh_active_tab()
759                         self._reset_tab_refresh()
760                 except Exception, e:
761                         self._errorDisplay.push_exception()
762                 return False
763
764         def _on_sms_clicked(self, number, message):
765                 try:
766                         assert number, "No number specified"
767                         assert message, "Empty message"
768                         try:
769                                 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
770                         except Exception, e:
771                                 loggedIn = False
772                                 self._errorDisplay.push_exception()
773                                 return
774
775                         if not loggedIn:
776                                 self._errorDisplay.push_message(
777                                         "Backend link with GoogleVoice is not working, please try again"
778                                 )
779                                 return
780
781                         dialed = False
782                         try:
783                                 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
784                                 hildonize.show_information_banner(self._window, "Sending to %s" % number)
785                                 dialed = True
786                         except Exception, e:
787                                 self._errorDisplay.push_exception()
788
789                         if dialed:
790                                 self._dialpads[self._selectedBackendId].clear()
791                 except Exception, e:
792                         self._errorDisplay.push_exception()
793
794         def _on_dial_clicked(self, number):
795                 try:
796                         assert number, "No number to call"
797                         try:
798                                 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
799                         except Exception, e:
800                                 loggedIn = False
801                                 self._errorDisplay.push_exception()
802                                 return
803
804                         if not loggedIn:
805                                 self._errorDisplay.push_message(
806                                         "Backend link with GoogleVoice is not working, please try again"
807                                 )
808                                 return
809
810                         dialed = False
811                         try:
812                                 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
813                                 self._phoneBackends[self._selectedBackendId].dial(number)
814                                 hildonize.show_information_banner(self._window, "Calling %s" % number)
815                                 dialed = True
816                         except Exception, e:
817                                 self._errorDisplay.push_exception()
818
819                         if dialed:
820                                 self._dialpads[self._selectedBackendId].clear()
821                 except Exception, e:
822                         self._errorDisplay.push_exception()
823
824         def _on_menu_refresh(self, *args):
825                 try:
826                         self._refresh_active_tab()
827                 except Exception, e:
828                         self._errorDisplay.push_exception()
829
830         def _on_paste(self, *args):
831                 try:
832                         contents = self._clipboard.wait_for_text()
833                         if contents is not None:
834                                 self._dialpads[self._selectedBackendId].set_number(contents)
835                 except Exception, e:
836                         self._errorDisplay.push_exception()
837
838         def _on_about_activate(self, *args):
839                 try:
840                         dlg = gtk.AboutDialog()
841                         dlg.set_name(constants.__pretty_app_name__)
842                         dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
843                         dlg.set_copyright("Copyright 2008 - LGPL")
844                         dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account.  This application is not affiliated with Google in any way")
845                         dlg.set_website("http://gc-dialer.garage.maemo.org/")
846                         dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
847                         dlg.run()
848                         dlg.destroy()
849                 except Exception, e:
850                         self._errorDisplay.push_exception()
851
852
853 def run_doctest():
854         import doctest
855
856         failureCount, testCount = doctest.testmod()
857         if not failureCount:
858                 print "Tests Successful"
859                 sys.exit(0)
860         else:
861                 sys.exit(1)
862
863
864 def run_dialpad():
865         _lock_file = os.path.join(constants._data_path_, ".lock")
866         #with gtk_toolbox.flock(_lock_file, 0):
867         gtk.gdk.threads_init()
868
869         if hildonize.IS_HILDON_SUPPORTED:
870                 gtk.set_application_name(constants.__pretty_app_name__)
871         handle = Dialcentral()
872         gtk.main()
873
874
875 class DummyOptions(object):
876
877         def __init__(self):
878                 self.test = False
879
880
881 if __name__ == "__main__":
882         logging.basicConfig(level=logging.DEBUG)
883         try:
884                 if len(sys.argv) > 1:
885                         try:
886                                 import optparse
887                         except ImportError:
888                                 optparse = None
889
890                         if optparse is not None:
891                                 parser = optparse.OptionParser()
892                                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
893                                 (commandOptions, commandArgs) = parser.parse_args()
894                 else:
895                         commandOptions = DummyOptions()
896                         commandArgs = []
897
898                 if commandOptions.test:
899                         run_doctest()
900                 else:
901                         run_dialpad()
902         finally:
903                 logging.shutdown()