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