Minor cleanup
[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 null_views
162                 import gc_views
163
164                 try:
165                         os.makedirs(self._data_path)
166                 except OSError, e:
167                         if e.errno != 17:
168                                 raise
169                 self._phoneBackends = [
170                         (gc_backend.GCDialer, os.path.join(self._data_path, "gc_cookies.txt")),
171                         (gv_backend.GVDialer, os.path.join(self._data_path, "gv_cookies.txt")),
172                 ]
173                 backendFactory, cookieFile = None, None
174                 for backendFactory, cookieFile in self._phoneBackends:
175                         if os.path.exists(cookieFile):
176                                 break
177                 else:
178                         backendFactory, cookieFile = self._phoneBackends[0]
179                 self._phoneBackend = backendFactory(cookieFile)
180                 gtk.gdk.threads_enter()
181                 try:
182                         self._dialpads = {
183                                 True: gc_views.Dialpad(self._widgetTree, self._errorDisplay),
184                                 False: null_views.Dialpad(self._widgetTree),
185                         }
186                         self._dialpads[True].set_number("")
187                         self._accountViews = {
188                                 True: gc_views.AccountInfo(self._widgetTree, self._phoneBackend, self._errorDisplay),
189                                 False: null_views.AccountInfo(self._widgetTree),
190                         }
191                         self._recentViews = {
192                                 True: gc_views.RecentCallsView(self._widgetTree, self._phoneBackend, self._errorDisplay),
193                                 False: null_views.RecentCallsView(self._widgetTree),
194                         }
195                         self._contactsViews = {
196                                 True: gc_views.ContactsView(self._widgetTree, self._phoneBackend, self._errorDisplay),
197                                 False: null_views.ContactsView(self._widgetTree),
198                         }
199                 finally:
200                         gtk.gdk.threads_leave()
201
202                 self._dialpads[True].dial = self._on_dial_clicked
203                 self._recentViews[True].number_selected = self._on_number_selected
204                 self._contactsViews[True].number_selected = self._on_number_selected
205
206                 fsContactsPath = os.path.join(self._data_path, "contacts")
207                 addressBooks = [
208                         self._phoneBackend,
209                         evo_backend.EvolutionAddressBook(),
210                         file_backend.FilesystemAddressBookFactory(fsContactsPath),
211                 ]
212                 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
213                 self._contactsViews[True].append(mergedBook)
214                 self._contactsViews[True].extend(addressBooks)
215                 self._contactsViews[True].open_addressbook(*self._contactsViews[True].get_addressbooks().next()[0][0:2])
216                 gtk.gdk.threads_enter()
217                 try:
218                         self._dialpads[self._isLoggedIn].enable()
219                         self._accountViews[self._isLoggedIn].enable()
220                         self._recentViews[self._isLoggedIn].enable()
221                         self._contactsViews[self._isLoggedIn].enable()
222                 finally:
223                         gtk.gdk.threads_leave()
224
225                 callbackMapping = {
226                         "on_paste": self._on_paste,
227                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
228                         "on_notebook_switch_page": self._on_notebook_switch_page,
229                         "on_about_activate": self._on_about_activate,
230                 }
231                 self._widgetTree.signal_autoconnect(callbackMapping)
232
233                 self.attempt_login(2)
234
235                 return False
236
237         def attempt_login(self, numOfAttempts = 1):
238                 """
239                 @todo Handle user notification better like attempting to login and failed login
240
241                 @note Not meant to be called directly, but run as a seperate thread.
242                 """
243                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
244
245                 if not self._deviceIsOnline:
246                         warnings.warn("Attempted to login while device was offline")
247                         return False
248                 elif self._phoneBackend is None:
249                         warnings.warn(
250                                 "Attempted to login before initialization is complete, did an event fire early?"
251                         )
252                         return False
253
254                 loggedIn = False
255                 try:
256                         if self._phoneBackend.is_authed():
257                                 loggedIn = True
258                         else:
259                                 for x in xrange(numOfAttempts):
260                                         gtk.gdk.threads_enter()
261                                         try:
262                                                 username, password = self._credentials.request_credentials()
263                                         finally:
264                                                 gtk.gdk.threads_leave()
265
266                                         loggedIn = self._phoneBackend.login(username, password)
267                                         if loggedIn:
268                                                 break
269                 except RuntimeError, e:
270                         warnings.warn(traceback.format_exc())
271                         gtk.gdk.threads_enter()
272                         try:
273                                 self._errorDisplay.push_message(e.message)
274                         finally:
275                                 gtk.gdk.threads_leave()
276
277                 gtk.gdk.threads_enter()
278                 try:
279                         if not loggedIn:
280                                 self._errorDisplay.push_message("Login Failed")
281                         self._change_loggedin_status(loggedIn)
282                 finally:
283                         gtk.gdk.threads_leave()
284                 return loggedIn
285
286         def display_error_message(self, msg):
287                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
288
289                 def close(dialog, response, editor):
290                         editor.about_dialog = None
291                         dialog.destroy()
292                 error_dialog.connect("response", close, self)
293                 error_dialog.run()
294
295         @staticmethod
296         def _on_close(*args, **kwds):
297                 gtk.main_quit()
298
299         def _change_loggedin_status(self, newStatus):
300                 oldStatus = self._isLoggedIn
301
302                 self._dialpads[oldStatus].disable()
303                 self._accountViews[oldStatus].disable()
304                 self._recentViews[oldStatus].disable()
305                 self._contactsViews[oldStatus].disable()
306
307                 self._dialpads[newStatus].enable()
308                 self._accountViews[newStatus].enable()
309                 self._recentViews[newStatus].enable()
310                 self._contactsViews[newStatus].enable()
311
312                 if newStatus:
313                         if self._phoneBackend.get_callback_number() is None:
314                                 self._phoneBackend.set_sane_callback()
315                         self._accountViews[True].update()
316
317                 self._isLoggedIn = newStatus
318
319         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
320                 """
321                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
322                 For system_inactivity, we have no background tasks to pause
323
324                 @note Hildon specific
325                 """
326                 if memory_low:
327                         self._phoneBackend.clear_caches()
328                         self._contactsViews[True].clear_caches()
329                         gc.collect()
330
331         def _on_connection_change(self, connection, event, magicIdentifier):
332                 """
333                 @note Hildon specific
334                 """
335                 import conic
336
337                 status = event.get_status()
338                 error = event.get_error()
339                 iap_id = event.get_iap_id()
340                 bearer = event.get_bearer_type()
341
342                 if status == conic.STATUS_CONNECTED:
343                         self._window.set_sensitive(True)
344                         self._deviceIsOnline = True
345                         self._isLoggedIn = False
346                         backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
347                         backgroundLogin.setDaemon(True)
348                         backgroundLogin.start()
349                 elif status == conic.STATUS_DISCONNECTED:
350                         self._window.set_sensitive(False)
351                         self._deviceIsOnline = False
352                         self._isLoggedIn = False
353
354         def _on_window_state_change(self, widget, event, *args):
355                 """
356                 @note Hildon specific
357                 """
358                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
359                         self._isFullScreen = True
360                 else:
361                         self._isFullScreen = False
362
363         def _on_key_press(self, widget, event, *args):
364                 """
365                 @note Hildon specific
366                 """
367                 if event.keyval == gtk.keysyms.F6:
368                         if self._isFullScreen:
369                                 self._window.unfullscreen()
370                         else:
371                                 self._window.fullscreen()
372
373         def _on_clearcookies_clicked(self, *args):
374                 self._phoneBackend.logout()
375                 self._accountViews[True].clear()
376                 self._recentViews[True].clear()
377                 self._contactsViews[True].clear()
378
379                 # re-run the inital grandcentral setup
380                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
381                 backgroundLogin.setDaemon(True)
382                 backgroundLogin.start()
383
384         def _on_notebook_switch_page(self, notebook, page, page_num):
385                 if page_num == 1:
386                         self._contactsViews[self._isLoggedIn].update()
387                 elif page_num == 3:
388                         self._recentViews[self._isLoggedIn].update()
389
390                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
391                 if hildon is not None:
392                         self._window.set_title(tabTitle)
393                 else:
394                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
395
396         def _on_number_selected(self, number):
397                 self._dialpads[True].set_number(number)
398                 self._notebook.set_current_page(0)
399
400         def _on_dial_clicked(self, number):
401                 """
402                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
403                 """
404                 try:
405                         loggedIn = self._phoneBackend.is_authed()
406                 except RuntimeError, e:
407                         warnings.warn(traceback.format_exc())
408                         loggedIn = False
409                         self._errorDisplay.push_message(e.message)
410                         return
411
412                 if not loggedIn:
413                         self._errorDisplay.push_message(
414                                 "Backend link with grandcentral is not working, please try again"
415                         )
416                         return
417
418                 dialed = False
419                 try:
420                         assert self._phoneBackend.get_callback_number() != ""
421                         self._phoneBackend.dial(number)
422                         dialed = True
423                 except RuntimeError, e:
424                         warnings.warn(traceback.format_exc())
425                         self._errorDisplay.push_message(e.message)
426                 except ValueError, e:
427                         warnings.warn(traceback.format_exc())
428                         self._errorDisplay.push_message(e.message)
429
430                 if dialed:
431                         self._dialpads[True].clear()
432                         self._recentViews[True].clear()
433
434         def _on_paste(self, *args):
435                 contents = self._clipboard.wait_for_text()
436                 self._dialpads[True].set_number(contents)
437
438         def _on_about_activate(self, *args):
439                 dlg = gtk.AboutDialog()
440                 dlg.set_name(self.__pretty_app_name__)
441                 dlg.set_version(self.__version__)
442                 dlg.set_copyright("Copyright 2008 - LGPL")
443                 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")
444                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
445                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
446                 dlg.run()
447                 dlg.destroy()
448
449
450 def run_doctest():
451         import doctest
452
453         failureCount, testCount = doctest.testmod()
454         if not failureCount:
455                 print "Tests Successful"
456                 sys.exit(0)
457         else:
458                 sys.exit(1)
459
460
461 def run_dialpad():
462         gtk.gdk.threads_init()
463         if hildon is not None:
464                 gtk.set_application_name(Dialcentral.__pretty_app_name__)
465         handle = Dialcentral()
466         gtk.main()
467
468
469 class DummyOptions(object):
470
471         def __init__(self):
472                 self.test = False
473
474
475 if __name__ == "__main__":
476         if len(sys.argv) > 1:
477                 try:
478                         import optparse
479                 except ImportError:
480                         optparse = None
481
482                 if optparse is not None:
483                         parser = optparse.OptionParser()
484                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
485                         (commandOptions, commandArgs) = parser.parse_args()
486         else:
487                 commandOptions = DummyOptions()
488                 commandArgs = []
489
490         if commandOptions.test:
491                 run_doctest()
492         else:
493                 run_dialpad()