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