Consolidating timeout location and increasing it for slower connections
[gc-dialer] / src / dc_glade.py
1 #!/usr/bin/python2.5
2
3 # DialCentral - Front end for Google's Grand Central service.
4 # Copyright (C) 2008  Mark Bergman bergman AT merctech DOT com
5
6 # This library is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU Lesser General Public
8 # License as published by the Free Software Foundation; either
9 # version 2.1 of the License, or (at your option) any later version.
10
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # Lesser General Public License for more details.
15
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with this library; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20
21 """
22 DialCentral: A phone dialer using GrandCentral
23 """
24
25 import sys
26 import gc
27 import os
28 import threading
29 import warnings
30 import traceback
31
32 import gtk
33 import gtk.glade
34
35 try:
36         import hildon
37 except ImportError:
38         hildon = None
39
40 import gtk_toolbox
41
42
43 class Dialcentral(object):
44
45         __pretty_app_name__ = "DialCentral"
46         __app_name__ = "dialcentral"
47         __version__ = "0.9.1"
48         __app_magic__ = 0xdeadbeef
49
50         _glade_files = [
51                 '/usr/lib/dialcentral/dialcentral.glade',
52                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
53                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
54         ]
55
56         _data_path = os.path.join(os.path.expanduser("~"), ".dialcentral")
57
58         def __init__(self):
59                 self._connection = None
60                 self._osso = None
61                 self._phoneBackends = []
62                 self._phoneBackend = None
63                 self._clipboard = gtk.clipboard_get()
64
65                 self._deviceIsOnline = True
66                 self._isLoggedIn = False
67                 self._dialpads = None
68                 self._accountViews = None
69                 self._recentViews = None
70                 self._contactsViews = None
71
72                 for path in Dialcentral._glade_files:
73                         if os.path.isfile(path):
74                                 self._widgetTree = gtk.glade.XML(path)
75                                 break
76                 else:
77                         self.display_error_message("Cannot find dialcentral.glade")
78                         gtk.main_quit()
79                         return
80
81                 self._window = self._widgetTree.get_widget("mainWindow")
82                 self._notebook = self._widgetTree.get_widget("notebook")
83                 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
84                 self._credentials = gtk_toolbox.LoginWindow(self._widgetTree)
85
86                 self._app = None
87                 self._isFullScreen = False
88                 if hildon is not None:
89                         self._app = hildon.Program()
90                         self._window = hildon.Window()
91                         self._widgetTree.get_widget("vbox1").reparent(self._window)
92                         self._app.add_window(self._window)
93                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
94                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
95                         self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
96                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
97                         hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
98
99                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
100                         menu = gtk.Menu()
101                         for child in gtkMenu.get_children():
102                                 child.reparent(menu)
103                         self._window.set_menu(menu)
104                         gtkMenu.destroy()
105
106                         self._window.connect("key-press-event", self._on_key_press)
107                         self._window.connect("window-state-event", self._on_window_state_change)
108                 else:
109                         pass # warnings.warn("No Hildon", UserWarning, 2)
110
111                 if hildon is not None:
112                         self._window.set_title("Keypad")
113                 else:
114                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
115
116                 callbackMapping = {
117                         "on_dialpad_quit": self._on_close,
118                 }
119                 self._widgetTree.signal_autoconnect(callbackMapping)
120
121                 if self._window:
122                         self._window.connect("destroy", gtk.main_quit)
123                         self._window.show_all()
124
125                 backgroundSetup = threading.Thread(target=self._idle_setup)
126                 backgroundSetup.setDaemon(True)
127                 backgroundSetup.start()
128
129         def _idle_setup(self):
130                 """
131                 If something can be done after the UI loads, push it here so it's not blocking the UI
132                 """
133                 try:
134                         import osso
135                 except ImportError:
136                         osso = None
137                 self._osso = None
138                 if osso is not None:
139                         self._osso = osso.Context(Dialcentral.__app_name__, Dialcentral.__version__, False)
140                         device = osso.DeviceState(self._osso)
141                         device.set_device_state_callback(self._on_device_state_change, 0)
142                 else:
143                         pass # warnings.warn("No OSSO", UserWarning)
144
145                 try:
146                         import conic
147                 except ImportError:
148                         conic = None
149                 self._connection = None
150                 if conic is not None:
151                         self._connection = conic.Connection()
152                         self._connection.connect("connection-event", self._on_connection_change, Dialcentral.__app_magic__)
153                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
154                 else:
155                         pass # warnings.warn("No Internet Connectivity API ", UserWarning)
156
157                 import gv_backend
158                 import gc_backend
159                 import file_backend
160                 import evo_backend
161                 # import gmail_backend
162                 # import maemo_backend
163                 import null_views
164                 import gc_views
165
166                 try:
167                         os.makedirs(self._data_path)
168                 except OSError, e:
169                         if e.errno != 17:
170                                 raise
171                 self._phoneBackends = [
172                         (gc_backend.GCDialer, os.path.join(self._data_path, "gc_cookies.txt")),
173                         (gv_backend.GVDialer, os.path.join(self._data_path, "gv_cookies.txt")),
174                 ]
175                 backendFactory, cookieFile = None, None
176                 for backendFactory, cookieFile in self._phoneBackends:
177                         if os.path.exists(cookieFile):
178                                 break
179                 else:
180                         backendFactory, cookieFile = self._phoneBackends[0]
181                 self._phoneBackend = backendFactory(cookieFile)
182                 gtk.gdk.threads_enter()
183                 try:
184                         self._dialpads = {
185                                 True: gc_views.Dialpad(self._widgetTree, self._errorDisplay),
186                                 False: null_views.Dialpad(self._widgetTree),
187                         }
188                         self._dialpads[True].set_number("")
189                         self._accountViews = {
190                                 True: gc_views.AccountInfo(self._widgetTree, self._phoneBackend, self._errorDisplay),
191                                 False: null_views.AccountInfo(self._widgetTree),
192                         }
193                         self._recentViews = {
194                                 True: gc_views.RecentCallsView(self._widgetTree, self._phoneBackend, self._errorDisplay),
195                                 False: null_views.RecentCallsView(self._widgetTree),
196                         }
197                         self._contactsViews = {
198                                 True: gc_views.ContactsView(self._widgetTree, self._phoneBackend, self._errorDisplay),
199                                 False: null_views.ContactsView(self._widgetTree),
200                         }
201                 finally:
202                         gtk.gdk.threads_leave()
203
204                 self._dialpads[True].dial = self._on_dial_clicked
205                 self._recentViews[True].number_selected = self._on_number_selected
206                 self._contactsViews[True].number_selected = self._on_number_selected
207
208                 fsContactsPath = os.path.join(self._data_path, "contacts")
209                 addressBooks = [
210                         self._phoneBackend,
211                         evo_backend.EvolutionAddressBook(),
212                         file_backend.FilesystemAddressBookFactory(fsContactsPath),
213                 ]
214                 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
215                 self._contactsViews[True].append(mergedBook)
216                 self._contactsViews[True].extend(addressBooks)
217                 self._contactsViews[True].open_addressbook(*self._contactsViews[True].get_addressbooks().next()[0][0:2])
218                 gtk.gdk.threads_enter()
219                 try:
220                         self._dialpads[self._isLoggedIn].enable()
221                         self._accountViews[self._isLoggedIn].enable()
222                         self._recentViews[self._isLoggedIn].enable()
223                         self._contactsViews[self._isLoggedIn].enable()
224                 finally:
225                         gtk.gdk.threads_leave()
226
227                 callbackMapping = {
228                         "on_paste": self._on_paste,
229                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
230                         "on_notebook_switch_page": self._on_notebook_switch_page,
231                         "on_about_activate": self._on_about_activate,
232                 }
233                 self._widgetTree.signal_autoconnect(callbackMapping)
234
235                 self.attempt_login(2)
236
237                 return False
238
239         def attempt_login(self, numOfAttempts = 1):
240                 """
241                 @todo Handle user notification better like attempting to login and failed login
242
243                 @note Not meant to be called directly, but run as a seperate thread.
244                 """
245                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
246
247                 if not self._deviceIsOnline:
248                         warnings.warn("Attempted to login while device was offline", UserWarning, 2)
249                         return False
250
251                 loggedIn = False
252                 try:
253                         if self._phoneBackend.is_authed():
254                                 loggedIn = True
255                         else:
256                                 for x in xrange(numOfAttempts):
257                                         gtk.gdk.threads_enter()
258                                         try:
259                                                 username, password = self._credentials.request_credentials()
260                                         finally:
261                                                 gtk.gdk.threads_leave()
262
263                                         loggedIn = self._phoneBackend.login(username, password)
264                                         if loggedIn:
265                                                 break
266                 except RuntimeError, e:
267                         warnings.warn(traceback.format_exc())
268                         gtk.gdk.threads_enter()
269                         try:
270                                 self._errorDisplay.push_message(e.message)
271                         finally:
272                                 gtk.gdk.threads_leave()
273
274                 gtk.gdk.threads_enter()
275                 try:
276                         if not loggedIn:
277                                 self._errorDisplay.push_message("Login Failed")
278                         self._change_loggedin_status(loggedIn)
279                 finally:
280                         gtk.gdk.threads_leave()
281                 return loggedIn
282
283         def display_error_message(self, msg):
284                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
285
286                 def close(dialog, response, editor):
287                         editor.about_dialog = None
288                         dialog.destroy()
289                 error_dialog.connect("response", close, self)
290                 error_dialog.run()
291
292         @staticmethod
293         def _on_close(*args, **kwds):
294                 gtk.main_quit()
295
296         def _change_loggedin_status(self, newStatus):
297                 oldStatus = self._isLoggedIn
298
299                 self._dialpads[oldStatus].disable()
300                 self._accountViews[oldStatus].disable()
301                 self._recentViews[oldStatus].disable()
302                 self._contactsViews[oldStatus].disable()
303
304                 self._dialpads[newStatus].enable()
305                 self._accountViews[newStatus].enable()
306                 self._recentViews[newStatus].enable()
307                 self._contactsViews[newStatus].enable()
308
309                 if newStatus:
310                         if self._phoneBackend.get_callback_number() is None:
311                                 self._phoneBackend.set_sane_callback()
312                         self._accountViews[True].update()
313
314                 self._isLoggedIn = newStatus
315
316         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
317                 """
318                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
319                 For system_inactivity, we have no background tasks to pause
320
321                 @note Hildon specific
322                 """
323                 if memory_low:
324                         self._phoneBackend.clear_caches()
325                         self._contactsViews[True].clear_caches()
326                         gc.collect()
327
328         def _on_connection_change(self, connection, event, magicIdentifier):
329                 """
330                 @note Hildon specific
331                 """
332                 import conic
333
334                 status = event.get_status()
335                 error = event.get_error()
336                 iap_id = event.get_iap_id()
337                 bearer = event.get_bearer_type()
338
339                 if status == conic.STATUS_CONNECTED:
340                         self._window.set_sensitive(True)
341                         self._deviceIsOnline = True
342                         self._isLoggedIn = False
343                         backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
344                         backgroundLogin.setDaemon(True)
345                         backgroundLogin.start()
346                 elif status == conic.STATUS_DISCONNECTED:
347                         self._window.set_sensitive(False)
348                         self._deviceIsOnline = False
349                         self._isLoggedIn = False
350
351         def _on_window_state_change(self, widget, event, *args):
352                 """
353                 @note Hildon specific
354                 """
355                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
356                         self._isFullScreen = True
357                 else:
358                         self._isFullScreen = False
359
360         def _on_key_press(self, widget, event, *args):
361                 """
362                 @note Hildon specific
363                 """
364                 if event.keyval == gtk.keysyms.F6:
365                         if self._isFullScreen:
366                                 self._window.unfullscreen()
367                         else:
368                                 self._window.fullscreen()
369
370         def _on_clearcookies_clicked(self, *args):
371                 self._phoneBackend.logout()
372                 self._accountViews[True].clear()
373                 self._recentViews[True].clear()
374                 self._contactsViews[True].clear()
375
376                 # re-run the inital grandcentral setup
377                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
378                 backgroundLogin.setDaemon(True)
379                 backgroundLogin.start()
380
381         def _on_notebook_switch_page(self, notebook, page, page_num):
382                 if page_num == 1:
383                         self._contactsViews[self._isLoggedIn].update()
384                 elif page_num == 3:
385                         self._recentViews[self._isLoggedIn].update()
386
387                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
388                 if hildon is not None:
389                         self._window.set_title(tabTitle)
390                 else:
391                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
392
393         def _on_number_selected(self, number):
394                 self._dialpads[True].set_number(number)
395                 self._notebook.set_current_page(0)
396
397         def _on_dial_clicked(self, number):
398                 """
399                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
400                 """
401                 try:
402                         loggedIn = self._phoneBackend.is_authed()
403                 except RuntimeError, e:
404                         warnings.warn(traceback.format_exc())
405                         loggedIn = False
406                         self._errorDisplay.push_message(e.message)
407                         return
408
409                 if not loggedIn:
410                         self._errorDisplay.push_message(
411                                 "Backend link with grandcentral is not working, please try again"
412                         )
413                         return
414
415                 dialed = False
416                 try:
417                         assert self._phoneBackend.get_callback_number() != ""
418                         self._phoneBackend.dial(number)
419                         dialed = True
420                 except RuntimeError, e:
421                         warnings.warn(traceback.format_exc())
422                         self._errorDisplay.push_message(e.message)
423                 except ValueError, e:
424                         warnings.warn(traceback.format_exc())
425                         self._errorDisplay.push_message(e.message)
426
427                 if dialed:
428                         self._dialpads[True].clear()
429                         self._recentViews[True].clear()
430
431         def _on_paste(self, *args):
432                 contents = self._clipboard.wait_for_text()
433                 self._dialpads[True].set_number(contents)
434
435         def _on_about_activate(self, *args):
436                 dlg = gtk.AboutDialog()
437                 dlg.set_name(self.__pretty_app_name__)
438                 dlg.set_version(self.__version__)
439                 dlg.set_copyright("Copyright 2008 - LGPL")
440                 dlg.set_comments("Dialer is designed to interface with your Google Grandcentral account.  This application is not affiliated with Google or Grandcentral in any way")
441                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
442                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
443                 dlg.run()
444                 dlg.destroy()
445
446
447 def run_doctest():
448         import doctest
449
450         failureCount, testCount = doctest.testmod()
451         if not failureCount:
452                 print "Tests Successful"
453                 sys.exit(0)
454         else:
455                 sys.exit(1)
456
457
458 def run_dialpad():
459         gtk.gdk.threads_init()
460         if hildon is not None:
461                 gtk.set_application_name(Dialcentral.__pretty_app_name__)
462         handle = Dialcentral()
463         gtk.main()
464
465
466 class DummyOptions(object):
467
468         def __init__(self):
469                 self.test = False
470
471
472 if __name__ == "__main__":
473         if len(sys.argv) > 1:
474                 try:
475                         import optparse
476                 except ImportError:
477                         optparse = None
478
479                 if optparse is not None:
480                         parser = optparse.OptionParser()
481                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
482                         (commandOptions, commandArgs) = parser.parse_args()
483         else:
484                 commandOptions = DummyOptions()
485                 commandArgs = []
486
487         if commandOptions.test:
488                 run_doctest()
489         else:
490                 run_dialpad()