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