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