Some build updates and moving some code out of gc_dialer.py
[gc-dialer] / src / gc_dialer.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
31 import gtk
32 import gtk.glade
33
34 try:
35         import hildon
36 except ImportError:
37         hildon = None
38
39
40 class Dialcentral(object):
41
42         __pretty_app_name__ = "DialCentral"
43         __app_name__ = "dialcentral"
44         __version__ = "0.8.4"
45         __app_magic__ = 0xdeadbeef
46
47         _glade_files = [
48                 '/usr/lib/dialcentral/dialcentral.glade',
49                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
50                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
51         ]
52
53         def __init__(self):
54                 self._gcBackend = None
55                 self._clipboard = gtk.clipboard_get()
56
57                 self._deviceIsOnline = True
58                 self._dialpad = None
59                 self._accountView = None
60                 self._recentView = None
61                 self._contactsView = None
62
63                 for path in Dialcentral._glade_files:
64                         if os.path.isfile(path):
65                                 self._widgetTree = gtk.glade.XML(path)
66                                 break
67                 else:
68                         self.display_error_message("Cannot find gc_dialer.glade")
69                         gtk.main_quit()
70                         return
71
72                 self._window = self._widgetTree.get_widget("Dialpad")
73                 self._notebook = self._widgetTree.get_widget("notebook")
74
75                 global hildon
76                 self._app = None
77                 self._isFullScreen = False
78                 if hildon is not None and self._window is gtk.Window:
79                         warnings.warn("Hildon installed but glade file not updated to work with hildon", UserWarning, 2)
80                         hildon = None
81                 elif hildon is not None:
82                         self._app = hildon.Program()
83                         self._window = hildon.Window()
84                         self._widgetTree.get_widget("vbox1").reparent(self._window)
85                         self._app.add_window(self._window)
86                         self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
87                         self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
88
89                         gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
90                         menu = gtk.Menu()
91                         for child in gtkMenu.get_children():
92                                 child.reparent(menu)
93                         self._window.set_menu(menu)
94                         gtkMenu.destroy()
95
96                         self._window.connect("key-press-event", self._on_key_press)
97                         self._window.connect("window-state-event", self._on_window_state_change)
98                 else:
99                         warnings.warn("No Hildon", UserWarning, 2)
100
101                 if hildon is not None:
102                         self._window.set_title("Keypad")
103                 else:
104                         self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
105
106                 callbackMapping = {
107                         "on_loginbutton_clicked": self._on_loginbutton_clicked,
108                         "on_loginclose_clicked": self._on_loginclose_clicked,
109                         "on_dialpad_quit": self._on_close,
110                 }
111                 self._widgetTree.signal_autoconnect(callbackMapping)
112
113                 if self._window:
114                         self._window.connect("destroy", gtk.main_quit)
115                         self._window.show_all()
116
117                 backgroundSetup = threading.Thread(target=self._idle_setup)
118                 backgroundSetup.setDaemon(True)
119                 backgroundSetup.start()
120
121
122         def _idle_setup(self):
123                 """
124                 If something can be done after the UI loads, push it here so it's not blocking the UI
125                 """
126                 try:
127                         import osso
128                 except ImportError:
129                         osso = None
130                 self._osso = None
131                 if osso is not None:
132                         self._osso = osso.Context(Dialcentral.__app_name__, Dialcentral.__version__, False)
133                         device = osso.DeviceState(self._osso)
134                         device.set_device_state_callback(self._on_device_state_change, 0)
135                 else:
136                         warnings.warn("No OSSO", UserWarning, 2)
137
138                 try:
139                         import conic
140                 except ImportError:
141                         conic = None
142                 self._connection = None
143                 if conic is not None:
144                         self._connection = conic.Connection()
145                         self._connection.connect("connection-event", self._on_connection_change, Dialcentral.__app_magic__)
146                         self._connection.request_connection(conic.CONNECT_FLAG_NONE)
147                 else:
148                         warnings.warn("No Internet Connectivity API ", UserWarning, 2)
149
150                 import gc_backend
151                 import evo_backend
152                 # import gmail_backend
153                 # import maemo_backend
154                 import views
155
156                 self._gcBackend = gc_backend.GCDialer()
157                 gtk.gdk.threads_enter()
158                 try:
159                         self._dialpad = views.Dialpad(self._widgetTree)
160                         self._dialpad.set_number("")
161                         self._accountView = views.AccountInfo(self._widgetTree, self._gcBackend)
162                         self._recentView = views.RecentCallsView(self._widgetTree, self._gcBackend)
163                         self._contactsView = views.ContactsView(self._widgetTree, self._gcBackend)
164                 finally:
165                         gtk.gdk.threads_leave()
166
167                 self._dialpad.dial = self._on_dial_clicked
168                 self._recentView.number_selected = self._on_number_selected
169                 self._contactsView.number_selected = self._on_number_selected
170
171                 #This is where the blocking can start
172                 if self._gcBackend.is_authed():
173                         gtk.gdk.threads_enter()
174                         try:
175                                 self._accountView.update()
176                         finally:
177                                 gtk.gdk.threads_leave()
178                 else:
179                         self.attempt_login(2)
180
181                 addressBooks = [
182                         self._gcBackend,
183                         evo_backend.EvolutionAddressBook(),
184                 ]
185                 mergedBook = views.MergedAddressBook(addressBooks, views.MergedAddressBook.basic_lastname_sorter)
186                 self._contactsView.append(mergedBook)
187                 self._contactsView.extend(addressBooks)
188                 self._contactsView.open_addressbook(*self._contactsView.get_addressbooks().next()[0][0:2])
189                 gtk.gdk.threads_enter()
190                 try:
191                         self._contactsView._init_books_combo()
192                 finally:
193                         gtk.gdk.threads_leave()
194
195                 callbackMapping = {
196                         "on_paste": self._on_paste,
197                         "on_clearcookies_clicked": self._on_clearcookies_clicked,
198                         "on_notebook_switch_page": self._on_notebook_switch_page,
199                         "on_about_activate": self._on_about_activate,
200                 }
201                 self._widgetTree.signal_autoconnect(callbackMapping)
202
203                 return False
204
205         def attempt_login(self, numOfAttempts = 1):
206                 """
207                 @todo Handle user notification better like attempting to login and failed login
208
209                 @note Not meant to be called directly, but run as a seperate thread.
210                 """
211                 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
212
213                 if not self._deviceIsOnline:
214                         warnings.warn("Attempted to login while device was offline", UserWarning, 2)
215                         return False
216
217                 if self._gcBackend.is_authed():
218                         return True
219
220                 for x in xrange(numOfAttempts):
221                         gtk.gdk.threads_enter()
222                         try:
223                                 dialog = self._widgetTree.get_widget("login_dialog")
224                                 dialog.set_transient_for(self._window)
225                                 dialog.set_default_response(0)
226                                 dialog.run()
227
228                                 username = self._widgetTree.get_widget("usernameentry").get_text()
229                                 password = self._widgetTree.get_widget("passwordentry").get_text()
230                                 self._widgetTree.get_widget("passwordentry").set_text("")
231                                 dialog.hide()
232                         finally:
233                                 gtk.gdk.threads_leave()
234                         loggedIn = self._gcBackend.login(username, password)
235                         if loggedIn:
236                                 gtk.gdk.threads_enter()
237                                 try:
238                                         if self._gcBackend.get_callback_number() is None:
239                                                 self._gcBackend.set_sane_callback()
240                                         self._accountView.update()
241                                 finally:
242                                         gtk.gdk.threads_leave()
243                                 return True
244
245                 return False
246
247         def display_error_message(self, msg):
248                 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
249
250                 def close(dialog, response, editor):
251                         editor.about_dialog = None
252                         dialog.destroy()
253                 error_dialog.connect("response", close, self)
254                 error_dialog.run()
255
256         @staticmethod
257         def _on_close(*args, **kwds):
258                 gtk.main_quit()
259
260         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
261                 """
262                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
263                 For system_inactivity, we have no background tasks to pause
264
265                 @note Hildon specific
266                 """
267                 if memory_low:
268                         self._gcBackend.clear_caches()
269                         self._contactsView.clear_caches()
270                         gc.collect()
271
272         def _on_connection_change(self, connection, event, magicIdentifier):
273                 """
274                 @note Hildon specific
275                 """
276                 import conic
277
278                 status = event.get_status()
279                 error = event.get_error()
280                 iap_id = event.get_iap_id()
281                 bearer = event.get_bearer_type()
282
283                 if status == conic.STATUS_CONNECTED:
284                         self._window.set_sensitive(True)
285                         self._deviceIsOnline = True
286                         backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
287                         backgroundLogin.setDaemon(True)
288                         backgroundLogin.start()
289                 elif status == conic.STATUS_DISCONNECTED:
290                         self._window.set_sensitive(False)
291                         self._deviceIsOnline = False
292
293         def _on_window_state_change(self, widget, event, *args):
294                 """
295                 @note Hildon specific
296                 """
297                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
298                         self._isFullScreen = True
299                 else:
300                         self._isFullScreen = False
301
302         def _on_key_press(self, widget, event, *args):
303                 """
304                 @note Hildon specific
305                 """
306                 if event.keyval == gtk.keysyms.F6:
307                         if self._isFullScreen:
308                                 self._window.unfullscreen()
309                         else:
310                                 self._window.fullscreen()
311
312         def _on_loginbutton_clicked(self, *args):
313                 self._widgetTree.get_widget("login_dialog").response(gtk.RESPONSE_OK)
314
315         def _on_loginclose_clicked(self, *args):
316                 self._on_close()
317                 sys.exit(0)
318
319         def _on_clearcookies_clicked(self, *args):
320                 self._gcBackend.logout()
321                 self._accountView.clear()
322                 self._recentView.clear()
323                 self._contactsView.clear()
324
325                 # re-run the inital grandcentral setup
326                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
327                 backgroundLogin.setDaemon(True)
328                 backgroundLogin.start()
329
330         def _on_notebook_switch_page(self, notebook, page, page_num):
331                 if page_num == 1:
332                         self._contactsView.update()
333                 elif page_num == 3:
334                         self._recentView.update()
335
336                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
337                 if hildon is not None:
338                         self._window.set_title(tabTitle)
339                 else:
340                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
341
342         def _on_number_selected(self, number):
343                 self._dialpad.set_number(number)
344                 self._notebook.set_current_page(0)
345
346         def _on_dial_clicked(self, number):
347                 """
348                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
349                 """
350                 loggedIn = self._gcBackend.is_authed()
351                 if not loggedIn:
352                         return
353                         #loggedIn = self.attempt_login(2)
354
355                 if not loggedIn or not self._gcBackend.is_authed() or self._gcBackend.get_callback_number() == "":
356                         self.display_error_message("Backend link with grandcentral is not working, please try again")
357                         warnings.warn("Backend Status: Logged in? %s, Authenticated? %s, Callback=%s" % (loggedIn, self._gcBackend.is_authed(), self._gcBackend.get_callback_number()), UserWarning, 2)
358                         return
359
360                 try:
361                         callSuccess = self._gcBackend.dial(number)
362                 except ValueError, e:
363                         self._gcBackend._msg = e.message
364                         callSuccess = False
365
366                 if not callSuccess:
367                         self.display_error_message(self._gcBackend._msg)
368                 else:
369                         self._dialpad.clear()
370
371                 self._recentView.clear()
372
373         def _on_paste(self, *args):
374                 contents = self._clipboard.wait_for_text()
375                 self._dialpad.set_number(contents)
376
377         def _on_about_activate(self, *args):
378                 dlg = gtk.AboutDialog()
379                 dlg.set_name(self.__pretty_app_name__)
380                 dlg.set_version(self.__version__)
381                 dlg.set_copyright("Copyright 2008 - LGPL")
382                 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")
383                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
384                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
385                 dlg.run()
386                 dlg.destroy()
387
388
389 def run_doctest():
390         import doctest
391
392         failureCount, testCount = doctest.testmod()
393         if not failureCount:
394                 print "Tests Successful"
395                 sys.exit(0)
396         else:
397                 sys.exit(1)
398
399
400 def run_dialpad():
401         gtk.gdk.threads_init()
402         if hildon is not None:
403                 gtk.set_application_name(Dialcentral.__pretty_app_name__)
404         handle = Dialcentral()
405         gtk.main()
406
407
408 class DummyOptions(object):
409
410         def __init__(self):
411                 self.test = False
412
413
414 if __name__ == "__main__":
415         if len(sys.argv) > 1:
416                 try:
417                         import optparse
418                 except ImportError:
419                         optparse = None
420
421                 if optparse is not None:
422                         parser = optparse.OptionParser()
423                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
424                         (commandOptions, commandArgs) = parser.parse_args()
425         else:
426                 commandOptions = DummyOptions()
427                 commandArgs = []
428
429         if commandOptions.test:
430                 run_doctest()
431         else:
432                 run_dialpad()