In theory, adding some hardening against bugs due to some reports on internet tablet...
[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")
249                         return False
250                 elif self._phoneBackend is None:
251                         warnings.warn(
252                                 "Attempted to login before initialization is complete, did an event fire early?"
253                         )
254                         return False
255
256                 loggedIn = False
257                 try:
258                         if self._phoneBackend.is_authed():
259                                 loggedIn = True
260                         else:
261                                 for x in xrange(numOfAttempts):
262                                         gtk.gdk.threads_enter()
263                                         try:
264                                                 username, password = self._credentials.request_credentials()
265                                         finally:
266                                                 gtk.gdk.threads_leave()
267
268                                         loggedIn = self._phoneBackend.login(username, password)
269                                         if loggedIn:
270                                                 break
271                 except RuntimeError, e:
272                         warnings.warn(traceback.format_exc())
273                         gtk.gdk.threads_enter()
274                         try:
275                                 self._errorDisplay.push_message(e.message)
276                         finally:
277                                 gtk.gdk.threads_leave()
278
279                 gtk.gdk.threads_enter()
280                 try:
281                         if not loggedIn:
282                                 self._errorDisplay.push_message("Login Failed")
283                         self._change_loggedin_status(loggedIn)
284                 finally:
285                         gtk.gdk.threads_leave()
286                 return loggedIn
287
288         def display_error_message(self, msg):
289                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
290
291                 def close(dialog, response, editor):
292                         editor.about_dialog = None
293                         dialog.destroy()
294                 error_dialog.connect("response", close, self)
295                 error_dialog.run()
296
297         @staticmethod
298         def _on_close(*args, **kwds):
299                 gtk.main_quit()
300
301         def _change_loggedin_status(self, newStatus):
302                 oldStatus = self._isLoggedIn
303
304                 self._dialpads[oldStatus].disable()
305                 self._accountViews[oldStatus].disable()
306                 self._recentViews[oldStatus].disable()
307                 self._contactsViews[oldStatus].disable()
308
309                 self._dialpads[newStatus].enable()
310                 self._accountViews[newStatus].enable()
311                 self._recentViews[newStatus].enable()
312                 self._contactsViews[newStatus].enable()
313
314                 if newStatus:
315                         if self._phoneBackend.get_callback_number() is None:
316                                 self._phoneBackend.set_sane_callback()
317                         self._accountViews[True].update()
318
319                 self._isLoggedIn = newStatus
320
321         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
322                 """
323                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
324                 For system_inactivity, we have no background tasks to pause
325
326                 @note Hildon specific
327                 """
328                 if memory_low:
329                         self._phoneBackend.clear_caches()
330                         self._contactsViews[True].clear_caches()
331                         gc.collect()
332
333         def _on_connection_change(self, connection, event, magicIdentifier):
334                 """
335                 @note Hildon specific
336                 """
337                 import conic
338
339                 status = event.get_status()
340                 error = event.get_error()
341                 iap_id = event.get_iap_id()
342                 bearer = event.get_bearer_type()
343
344                 if status == conic.STATUS_CONNECTED:
345                         self._window.set_sensitive(True)
346                         self._deviceIsOnline = True
347                         self._isLoggedIn = False
348                         backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
349                         backgroundLogin.setDaemon(True)
350                         backgroundLogin.start()
351                 elif status == conic.STATUS_DISCONNECTED:
352                         self._window.set_sensitive(False)
353                         self._deviceIsOnline = False
354                         self._isLoggedIn = False
355
356         def _on_window_state_change(self, widget, event, *args):
357                 """
358                 @note Hildon specific
359                 """
360                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
361                         self._isFullScreen = True
362                 else:
363                         self._isFullScreen = False
364
365         def _on_key_press(self, widget, event, *args):
366                 """
367                 @note Hildon specific
368                 """
369                 if event.keyval == gtk.keysyms.F6:
370                         if self._isFullScreen:
371                                 self._window.unfullscreen()
372                         else:
373                                 self._window.fullscreen()
374
375         def _on_clearcookies_clicked(self, *args):
376                 self._phoneBackend.logout()
377                 self._accountViews[True].clear()
378                 self._recentViews[True].clear()
379                 self._contactsViews[True].clear()
380
381                 # re-run the inital grandcentral setup
382                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
383                 backgroundLogin.setDaemon(True)
384                 backgroundLogin.start()
385
386         def _on_notebook_switch_page(self, notebook, page, page_num):
387                 if page_num == 1:
388                         self._contactsViews[self._isLoggedIn].update()
389                 elif page_num == 3:
390                         self._recentViews[self._isLoggedIn].update()
391
392                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
393                 if hildon is not None:
394                         self._window.set_title(tabTitle)
395                 else:
396                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
397
398         def _on_number_selected(self, number):
399                 self._dialpads[True].set_number(number)
400                 self._notebook.set_current_page(0)
401
402         def _on_dial_clicked(self, number):
403                 """
404                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
405                 """
406                 try:
407                         loggedIn = self._phoneBackend.is_authed()
408                 except RuntimeError, e:
409                         warnings.warn(traceback.format_exc())
410                         loggedIn = False
411                         self._errorDisplay.push_message(e.message)
412                         return
413
414                 if not loggedIn:
415                         self._errorDisplay.push_message(
416                                 "Backend link with grandcentral is not working, please try again"
417                         )
418                         return
419
420                 dialed = False
421                 try:
422                         assert self._phoneBackend.get_callback_number() != ""
423                         self._phoneBackend.dial(number)
424                         dialed = True
425                 except RuntimeError, e:
426                         warnings.warn(traceback.format_exc())
427                         self._errorDisplay.push_message(e.message)
428                 except ValueError, e:
429                         warnings.warn(traceback.format_exc())
430                         self._errorDisplay.push_message(e.message)
431
432                 if dialed:
433                         self._dialpads[True].clear()
434                         self._recentViews[True].clear()
435
436         def _on_paste(self, *args):
437                 contents = self._clipboard.wait_for_text()
438                 self._dialpads[True].set_number(contents)
439
440         def _on_about_activate(self, *args):
441                 dlg = gtk.AboutDialog()
442                 dlg.set_name(self.__pretty_app_name__)
443                 dlg.set_version(self.__version__)
444                 dlg.set_copyright("Copyright 2008 - LGPL")
445                 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")
446                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
447                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
448                 dlg.run()
449                 dlg.destroy()
450
451
452 def run_doctest():
453         import doctest
454
455         failureCount, testCount = doctest.testmod()
456         if not failureCount:
457                 print "Tests Successful"
458                 sys.exit(0)
459         else:
460                 sys.exit(1)
461
462
463 def run_dialpad():
464         gtk.gdk.threads_init()
465         if hildon is not None:
466                 gtk.set_application_name(Dialcentral.__pretty_app_name__)
467         handle = Dialcentral()
468         gtk.main()
469
470
471 class DummyOptions(object):
472
473         def __init__(self):
474                 self.test = False
475
476
477 if __name__ == "__main__":
478         if len(sys.argv) > 1:
479                 try:
480                         import optparse
481                 except ImportError:
482                         optparse = None
483
484                 if optparse is not None:
485                         parser = optparse.OptionParser()
486                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
487                         (commandOptions, commandArgs) = parser.parse_args()
488         else:
489                 commandOptions = DummyOptions()
490                 commandArgs = []
491
492         if commandOptions.test:
493                 run_doctest()
494         else:
495                 run_dialpad()