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