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