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