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