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