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"),
68 BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
70 _data_path = os.path.join(os.path.expanduser("~"), ".dialcentral")
73 self._connection = None
75 self._clipboard = gtk.clipboard_get()
77 self._deviceIsOnline = True
78 self._selectedBackendId = self.NULL_BACKEND
79 self._defaultBackendId = self.GC_BACKEND
80 self._phoneBackends = None
82 self._accountViews = None
83 self._recentViews = None
84 self._contactsViews = None
86 for path in self._glade_files:
87 if os.path.isfile(path):
88 self._widgetTree = gtk.glade.XML(path)
91 self.display_error_message("Cannot find dialcentral.glade")
95 self._window = self._widgetTree.get_widget("mainWindow")
96 self._notebook = self._widgetTree.get_widget("notebook")
97 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
98 self._credentials = gtk_toolbox.LoginWindow(self._widgetTree)
101 self._isFullScreen = False
102 if hildon is not None:
103 self._app = hildon.Program()
104 self._window = hildon.Window()
105 self._widgetTree.get_widget("vbox1").reparent(self._window)
106 self._app.add_window(self._window)
107 self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
108 self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
109 self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
110 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
111 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
113 gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
115 for child in gtkMenu.get_children():
117 self._window.set_menu(menu)
120 self._window.connect("key-press-event", self._on_key_press)
121 self._window.connect("window-state-event", self._on_window_state_change)
123 pass # warnings.warn("No Hildon", UserWarning, 2)
125 if hildon is not None:
126 self._window.set_title("Keypad")
128 self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
131 "on_dialpad_quit": self._on_close,
133 self._widgetTree.signal_autoconnect(callbackMapping)
136 self._window.connect("destroy", gtk.main_quit)
137 self._window.show_all()
138 self._window.set_default_size(800, 300)
140 backgroundSetup = threading.Thread(target=self._idle_setup)
141 backgroundSetup.setDaemon(True)
142 backgroundSetup.start()
144 def _idle_setup(self):
146 If something can be done after the UI loads, push it here so it's not blocking the UI
148 # Barebones UI handlers
152 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
153 with gtk_toolbox.gtk_lock():
154 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
155 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
156 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
157 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
159 self._dialpads[self._selectedBackendId].enable()
160 self._accountViews[self._selectedBackendId].enable()
161 self._recentViews[self._selectedBackendId].enable()
162 self._contactsViews[self._selectedBackendId].enable()
164 # Setup maemo specifics
171 self._osso = osso.Context(Dialcentral.__app_name__, Dialcentral.__version__, False)
172 device = osso.DeviceState(self._osso)
173 device.set_device_state_callback(self._on_device_state_change, 0)
175 pass # warnings.warn("No OSSO", UserWarning)
181 self._connection = None
182 if conic is not None:
183 self._connection = conic.Connection()
184 self._connection.connect("connection-event", self._on_connection_change, Dialcentral.__app_magic__)
185 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
187 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
189 # Setup costly backends
197 os.makedirs(self._data_path)
201 gcCookiePath = os.path.join(self._data_path, "gc_cookies.txt")
202 gvCookiePath = os.path.join(self._data_path, "gv_cookies.txt")
203 self._defaultBackendId = self._guess_preferred_backend((
204 (self.GC_BACKEND, gcCookiePath),
205 (self.GV_BACKEND, gvCookiePath),
208 self._phoneBackends.update({
209 self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
210 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
212 with gtk_toolbox.gtk_lock():
213 unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
214 unifiedDialpad.set_number("")
215 self._dialpads.update({
216 self.GC_BACKEND: unifiedDialpad,
217 self.GV_BACKEND: unifiedDialpad,
219 self._accountViews.update({
220 self.GC_BACKEND: gc_views.AccountInfo(
221 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
223 self.GV_BACKEND: gc_views.AccountInfo(
224 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
227 self._recentViews.update({
228 self.GC_BACKEND: gc_views.RecentCallsView(
229 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
231 self.GV_BACKEND: gc_views.RecentCallsView(
232 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
235 self._contactsViews.update({
236 self.GC_BACKEND: gc_views.ContactsView(
237 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
239 self.GV_BACKEND: gc_views.ContactsView(
240 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
244 evoBackend = evo_backend.EvolutionAddressBook()
245 fsContactsPath = os.path.join(self._data_path, "contacts")
246 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
247 for backendId in (self.GV_BACKEND, self.GC_BACKEND):
248 self._dialpads[backendId].dial = self._on_dial_clicked
249 self._recentViews[backendId].number_selected = self._on_number_selected
250 self._contactsViews[backendId].number_selected = self._on_number_selected
253 self._phoneBackends[backendId],
257 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
258 self._contactsViews[backendId].append(mergedBook)
259 self._contactsViews[backendId].extend(addressBooks)
260 self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
263 "on_paste": self._on_paste,
264 "on_clearcookies_clicked": self._on_clearcookies_clicked,
265 "on_notebook_switch_page": self._on_notebook_switch_page,
266 "on_about_activate": self._on_about_activate,
268 self._widgetTree.signal_autoconnect(callbackMapping)
270 self.attempt_login(2)
274 def attempt_login(self, numOfAttempts = 10):
276 @todo Handle user notification better like attempting to login and failed login
278 @note Not meant to be called directly, but run as a seperate thread.
280 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
282 if not self._deviceIsOnline:
283 warnings.warn("Attempted to login while device was offline")
285 elif self._phoneBackends is None or len(self._phoneBackends) < len(self.BACKENDS):
287 "Attempted to login before initialization is complete, did an event fire early?"
293 if self._phoneBackends[self._defaultBackendId].is_authed():
294 serviceId = self._defaultBackendId
296 for x in xrange(numOfAttempts):
299 with gtk_toolbox.gtk_lock():
300 availableServices = {
301 self.GV_BACKEND: "Google Voice",
302 self.GC_BACKEND: "Grand Central",
304 credentials = self._credentials.request_credentials_from(availableServices)
305 serviceId, username, password = credentials
307 loggedIn = self._phoneBackends[serviceId].login(username, password)
308 except RuntimeError, e:
309 warnings.warn(traceback.format_exc())
310 self._errorDisplay.push_exception_with_lock(e)
312 with gtk_toolbox.gtk_lock():
314 self._errorDisplay.push_message("Login Failed")
315 self._change_loggedin_status(serviceId if loggedIn else self.NULL_BACKEND)
318 def display_error_message(self, msg):
319 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
321 def close(dialog, response, editor):
322 editor.about_dialog = None
324 error_dialog.connect("response", close, self)
327 def _on_close(self, *args, **kwds):
328 if self._osso is not None:
332 def _change_loggedin_status(self, newStatus):
333 oldStatus = self._selectedBackendId
334 if oldStatus == newStatus:
337 self._dialpads[oldStatus].disable()
338 self._accountViews[oldStatus].disable()
339 self._recentViews[oldStatus].disable()
340 self._contactsViews[oldStatus].disable()
342 self._dialpads[newStatus].enable()
343 self._accountViews[newStatus].enable()
344 self._recentViews[newStatus].enable()
345 self._contactsViews[newStatus].enable()
347 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
348 self._phoneBackends[self._selectedBackendId].set_sane_callback()
349 self._accountViews[self._selectedBackendId].update()
351 self._selectedBackendId = newStatus
353 def _guess_preferred_backend(self, backendAndCookiePaths):
355 (getmtime_nothrow(path), backendId, path)
356 for backendId, path in backendAndCookiePaths
358 modTimeAndPath.sort()
359 return modTimeAndPath[-1][1]
361 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
363 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
364 For system_inactivity, we have no background tasks to pause
366 @note Hildon specific
369 for backendId in self.BACKENDS:
370 self._phoneBackends[backendId].clear_caches()
371 self._contactsViews[self._selectedBackendId].clear_caches()
374 def _on_connection_change(self, connection, event, magicIdentifier):
376 @note Hildon specific
380 status = event.get_status()
381 error = event.get_error()
382 iap_id = event.get_iap_id()
383 bearer = event.get_bearer_type()
385 if status == conic.STATUS_CONNECTED:
386 self._deviceIsOnline = True
387 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
388 backgroundLogin.setDaemon(True)
389 backgroundLogin.start()
390 elif status == conic.STATUS_DISCONNECTED:
391 self._deviceIsOnline = False
392 self._defaultBackendId = self._selectedBackendId
393 self._change_loggedin_status(self.NULL_BACKEND)
395 def _on_window_state_change(self, widget, event, *args):
397 @note Hildon specific
399 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
400 self._isFullScreen = True
402 self._isFullScreen = False
404 def _on_key_press(self, widget, event, *args):
406 @note Hildon specific
408 if event.keyval == gtk.keysyms.F6:
409 if self._isFullScreen:
410 self._window.unfullscreen()
412 self._window.fullscreen()
414 def _on_clearcookies_clicked(self, *args):
415 self._phoneBackends[self._selectedBackendId].logout()
416 self._accountViews[self._selectedBackendId].clear()
417 self._recentViews[self._selectedBackendId].clear()
418 self._contactsViews[self._selectedBackendId].clear()
419 self._change_loggedin_status(self.NULL_BACKEND)
421 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
422 backgroundLogin.setDaemon(True)
423 backgroundLogin.start()
425 def _on_notebook_switch_page(self, notebook, page, page_num):
427 self._contactsViews[self._selectedBackendId].update()
429 self._recentViews[self._selectedBackendId].update()
431 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
432 if hildon is not None:
433 self._window.set_title(tabTitle)
435 self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
437 def _on_number_selected(self, number):
438 self._dialpads[self._selectedBackendId].set_number(number)
439 self._notebook.set_current_page(0)
441 def _on_dial_clicked(self, number):
443 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
446 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
447 except RuntimeError, e:
449 self._errorDisplay.push_exception(e)
453 self._errorDisplay.push_message(
454 "Backend link with grandcentral is not working, please try again"
460 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != ""
461 self._phoneBackends[self._selectedBackendId].dial(number)
463 except RuntimeError, e:
464 self._errorDisplay.push_exception(e)
465 except ValueError, e:
466 self._errorDisplay.push_exception(e)
469 self._dialpads[self._selectedBackendId].clear()
470 self._recentViews[self._selectedBackendId].clear()
472 def _on_paste(self, *args):
473 contents = self._clipboard.wait_for_text()
474 self._dialpads[self._selectedBackendId].set_number(contents)
476 def _on_about_activate(self, *args):
477 dlg = gtk.AboutDialog()
478 dlg.set_name(self.__pretty_app_name__)
479 dlg.set_version(self.__version__)
480 dlg.set_copyright("Copyright 2008 - LGPL")
481 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")
482 dlg.set_website("http://gc-dialer.garage.maemo.org/")
483 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
491 failureCount, testCount = doctest.testmod()
493 print "Tests Successful"
500 gtk.gdk.threads_init()
501 if hildon is not None:
502 gtk.set_application_name(Dialcentral.__pretty_app_name__)
503 handle = Dialcentral()
507 class DummyOptions(object):
513 if __name__ == "__main__":
514 if len(sys.argv) > 1:
520 if optparse is not None:
521 parser = optparse.OptionParser()
522 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
523 (commandOptions, commandArgs) = parser.parse_args()
525 commandOptions = DummyOptions()
528 if commandOptions.test: