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