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