4 DialCentral - Front end for Google's Grand Central service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 @todo Add logging support to make debugging issues for people a lot easier
25 from __future__ import with_statement
45 def getmtime_nothrow(path):
47 return os.path.getmtime(path)
52 class Dialcentral(object):
54 __pretty_app_name__ = "DialCentral"
55 __app_name__ = "dialcentral"
57 __app_magic__ = 0xdeadbeef
60 '/usr/lib/dialcentral/dialcentral.glade',
61 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
62 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
73 BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
75 _data_path = os.path.join(os.path.expanduser("~"), ".dialcentral")
78 self._connection = None
80 self._clipboard = gtk.clipboard_get()
82 self._deviceIsOnline = True
83 self._selectedBackendId = self.NULL_BACKEND
84 self._defaultBackendId = self.GC_BACKEND
85 self._phoneBackends = None
87 self._accountViews = None
88 self._recentViews = None
89 self._contactsViews = None
91 for path in self._glade_files:
92 if os.path.isfile(path):
93 self._widgetTree = gtk.glade.XML(path)
96 self.display_error_message("Cannot find dialcentral.glade")
100 self._window = self._widgetTree.get_widget("mainWindow")
101 self._notebook = self._widgetTree.get_widget("notebook")
102 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
103 self._credentials = gtk_toolbox.LoginWindow(self._widgetTree)
106 self._isFullScreen = False
107 if hildon is not None:
108 self._app = hildon.Program()
109 self._window = hildon.Window()
110 self._widgetTree.get_widget("vbox1").reparent(self._window)
111 self._app.add_window(self._window)
112 self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
113 self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
114 self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
115 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
116 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
118 gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
120 for child in gtkMenu.get_children():
122 self._window.set_menu(menu)
125 self._window.connect("key-press-event", self._on_key_press)
126 self._window.connect("window-state-event", self._on_window_state_change)
128 pass # warnings.warn("No Hildon", UserWarning, 2)
130 if hildon is not None:
131 self._window.set_title("Keypad")
133 self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
136 "on_dialpad_quit": self._on_close,
138 self._widgetTree.signal_autoconnect(callbackMapping)
141 self._window.connect("destroy", gtk.main_quit)
142 self._window.show_all()
143 self._window.set_default_size(800, 300)
145 backgroundSetup = threading.Thread(target=self._idle_setup)
146 backgroundSetup.setDaemon(True)
147 backgroundSetup.start()
149 def _idle_setup(self):
151 If something can be done after the UI loads, push it here so it's not blocking the UI
153 # Barebones UI handlers
157 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
158 with gtk_toolbox.gtk_lock():
159 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
160 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
161 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
162 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
164 self._dialpads[self._selectedBackendId].enable()
165 self._accountViews[self._selectedBackendId].enable()
166 self._recentViews[self._selectedBackendId].enable()
167 self._contactsViews[self._selectedBackendId].enable()
169 # Setup maemo specifics
176 self._osso = osso.Context(Dialcentral.__app_name__, Dialcentral.__version__, False)
177 device = osso.DeviceState(self._osso)
178 device.set_device_state_callback(self._on_device_state_change, 0)
180 pass # warnings.warn("No OSSO", UserWarning)
186 self._connection = None
187 if conic is not None:
188 self._connection = conic.Connection()
189 self._connection.connect("connection-event", self._on_connection_change, Dialcentral.__app_magic__)
190 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
192 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
194 # Setup costly backends
202 os.makedirs(self._data_path)
206 gcCookiePath = os.path.join(self._data_path, "gc_cookies.txt")
207 gvCookiePath = os.path.join(self._data_path, "gv_cookies.txt")
208 self._defaultBackendId = self._guess_preferred_backend((
209 (self.GC_BACKEND, gcCookiePath),
210 (self.GV_BACKEND, gvCookiePath),
213 self._phoneBackends.update({
214 self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
215 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
217 with gtk_toolbox.gtk_lock():
218 unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
219 unifiedDialpad.set_number("")
220 self._dialpads.update({
221 self.GC_BACKEND: unifiedDialpad,
222 self.GV_BACKEND: unifiedDialpad,
224 self._accountViews.update({
225 self.GC_BACKEND: gc_views.AccountInfo(
226 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
228 self.GV_BACKEND: gc_views.AccountInfo(
229 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
232 self._recentViews.update({
233 self.GC_BACKEND: gc_views.RecentCallsView(
234 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
236 self.GV_BACKEND: gc_views.RecentCallsView(
237 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
240 self._contactsViews.update({
241 self.GC_BACKEND: gc_views.ContactsView(
242 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
244 self.GV_BACKEND: gc_views.ContactsView(
245 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
249 evoBackend = evo_backend.EvolutionAddressBook()
250 fsContactsPath = os.path.join(self._data_path, "contacts")
251 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
252 for backendId in (self.GV_BACKEND, self.GC_BACKEND):
253 self._dialpads[backendId].dial = self._on_dial_clicked
254 self._recentViews[backendId].number_selected = self._on_number_selected
255 self._contactsViews[backendId].number_selected = self._on_number_selected
258 self._phoneBackends[backendId],
262 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
263 self._contactsViews[backendId].append(mergedBook)
264 self._contactsViews[backendId].extend(addressBooks)
265 self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
268 "on_paste": self._on_paste,
269 "on_clearcookies_clicked": self._on_clearcookies_clicked,
270 "on_notebook_switch_page": self._on_notebook_switch_page,
271 "on_about_activate": self._on_about_activate,
273 self._widgetTree.signal_autoconnect(callbackMapping)
275 self.attempt_login(2)
279 def attempt_login(self, numOfAttempts = 10):
281 @todo Handle user notification better like attempting to login and failed login
283 @note Not meant to be called directly, but run as a seperate thread.
285 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
287 if not self._deviceIsOnline:
288 warnings.warn("Attempted to login while device was offline")
290 elif self._phoneBackends is None or len(self._phoneBackends) < len(self.BACKENDS):
292 "Attempted to login before initialization is complete, did an event fire early?"
298 if self._phoneBackends[self._defaultBackendId].is_authed():
299 serviceId = self._defaultBackendId
301 for x in xrange(numOfAttempts):
304 with gtk_toolbox.gtk_lock():
305 availableServices = {
306 self.GV_BACKEND: "Google Voice",
307 self.GC_BACKEND: "Grand Central",
309 credentials = self._credentials.request_credentials_from(availableServices)
310 serviceId, username, password = credentials
312 loggedIn = self._phoneBackends[serviceId].login(username, password)
313 except RuntimeError, e:
314 warnings.warn(traceback.format_exc())
315 self._errorDisplay.push_exception_with_lock(e)
317 with gtk_toolbox.gtk_lock():
319 self._errorDisplay.push_message("Login Failed")
320 self._change_loggedin_status(serviceId if loggedIn else self.NULL_BACKEND)
323 def display_error_message(self, msg):
324 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
326 def close(dialog, response, editor):
327 editor.about_dialog = None
329 error_dialog.connect("response", close, self)
332 def _on_close(self, *args, **kwds):
333 if self._osso is not None:
337 def _change_loggedin_status(self, newStatus):
338 oldStatus = self._selectedBackendId
339 if oldStatus == newStatus:
342 self._dialpads[oldStatus].disable()
343 self._accountViews[oldStatus].disable()
344 self._recentViews[oldStatus].disable()
345 self._contactsViews[oldStatus].disable()
347 self._dialpads[newStatus].enable()
348 self._accountViews[newStatus].enable()
349 self._recentViews[newStatus].enable()
350 self._contactsViews[newStatus].enable()
352 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
353 self._phoneBackends[self._selectedBackendId].set_sane_callback()
354 self._accountViews[self._selectedBackendId].update()
356 self._selectedBackendId = newStatus
358 def _guess_preferred_backend(self, backendAndCookiePaths):
360 (getmtime_nothrow(path), backendId, path)
361 for backendId, path in backendAndCookiePaths
363 modTimeAndPath.sort()
364 return modTimeAndPath[-1][1]
366 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
368 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
369 For system_inactivity, we have no background tasks to pause
371 @note Hildon specific
374 for backendId in self.BACKENDS:
375 self._phoneBackends[backendId].clear_caches()
376 self._contactsViews[self._selectedBackendId].clear_caches()
379 def _on_connection_change(self, connection, event, magicIdentifier):
381 @note Hildon specific
385 status = event.get_status()
386 error = event.get_error()
387 iap_id = event.get_iap_id()
388 bearer = event.get_bearer_type()
390 if status == conic.STATUS_CONNECTED:
391 self._deviceIsOnline = True
392 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
393 backgroundLogin.setDaemon(True)
394 backgroundLogin.start()
395 elif status == conic.STATUS_DISCONNECTED:
396 self._deviceIsOnline = False
397 self._defaultBackendId = self._selectedBackendId
398 self._change_loggedin_status(self.NULL_BACKEND)
400 def _on_window_state_change(self, widget, event, *args):
402 @note Hildon specific
404 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
405 self._isFullScreen = True
407 self._isFullScreen = False
409 def _on_key_press(self, widget, event, *args):
411 @note Hildon specific
413 if event.keyval == gtk.keysyms.F6:
414 if self._isFullScreen:
415 self._window.unfullscreen()
417 self._window.fullscreen()
419 def _on_clearcookies_clicked(self, *args):
420 self._phoneBackends[self._selectedBackendId].logout()
421 self._accountViews[self._selectedBackendId].clear()
422 self._recentViews[self._selectedBackendId].clear()
423 self._contactsViews[self._selectedBackendId].clear()
424 self._change_loggedin_status(self.NULL_BACKEND)
426 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
427 backgroundLogin.setDaemon(True)
428 backgroundLogin.start()
430 def _on_notebook_switch_page(self, notebook, page, page_num):
431 if page_num == self.CONTACTS_TAB:
432 self._contactsViews[self._selectedBackendId].update()
433 elif page_num == self.RECENT_TAB:
434 self._recentViews[self._selectedBackendId].update()
436 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
437 if hildon is not None:
438 self._window.set_title(tabTitle)
440 self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
442 def _on_number_selected(self, number):
443 self._dialpads[self._selectedBackendId].set_number(number)
444 self._notebook.set_current_page(0)
446 def _on_dial_clicked(self, number):
448 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
451 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
452 except RuntimeError, e:
454 self._errorDisplay.push_exception(e)
458 self._errorDisplay.push_message(
459 "Backend link with grandcentral is not working, please try again"
465 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != ""
466 self._phoneBackends[self._selectedBackendId].dial(number)
468 except RuntimeError, e:
469 self._errorDisplay.push_exception(e)
470 except ValueError, e:
471 self._errorDisplay.push_exception(e)
474 self._dialpads[self._selectedBackendId].clear()
475 self._recentViews[self._selectedBackendId].clear()
477 def _on_paste(self, *args):
478 contents = self._clipboard.wait_for_text()
479 self._dialpads[self._selectedBackendId].set_number(contents)
481 def _on_about_activate(self, *args):
482 dlg = gtk.AboutDialog()
483 dlg.set_name(self.__pretty_app_name__)
484 dlg.set_version(self.__version__)
485 dlg.set_copyright("Copyright 2008 - LGPL")
486 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")
487 dlg.set_website("http://gc-dialer.garage.maemo.org/")
488 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
496 failureCount, testCount = doctest.testmod()
498 print "Tests Successful"
505 gtk.gdk.threads_init()
506 if hildon is not None:
507 gtk.set_application_name(Dialcentral.__pretty_app_name__)
508 handle = Dialcentral()
512 class DummyOptions(object):
518 if __name__ == "__main__":
519 if len(sys.argv) > 1:
525 if optparse is not None:
526 parser = optparse.OptionParser()
527 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
528 (commandOptions, commandArgs) = parser.parse_args()
530 commandOptions = DummyOptions()
533 if commandOptions.test: