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"),
74 BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
76 _data_path = os.path.join(os.path.expanduser("~"), ".dialcentral")
79 self._connection = None
81 self._clipboard = gtk.clipboard_get()
83 self._deviceIsOnline = True
84 self._selectedBackendId = self.NULL_BACKEND
85 self._defaultBackendId = self.GC_BACKEND
86 self._phoneBackends = None
88 self._accountViews = None
89 self._messagesViews = None
90 self._recentViews = None
91 self._contactsViews = None
93 for path in self._glade_files:
94 if os.path.isfile(path):
95 self._widgetTree = gtk.glade.XML(path)
98 self.display_error_message("Cannot find dialcentral.glade")
102 self._window = self._widgetTree.get_widget("mainWindow")
103 self._notebook = self._widgetTree.get_widget("notebook")
104 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
105 self._credentials = gtk_toolbox.LoginWindow(self._widgetTree)
108 self._isFullScreen = False
109 if hildon is not None:
110 self._app = hildon.Program()
111 self._window = hildon.Window()
112 self._widgetTree.get_widget("vbox1").reparent(self._window)
113 self._app.add_window(self._window)
114 self._widgetTree.get_widget("usernameentry").set_property('hildon-input-mode', 7)
115 self._widgetTree.get_widget("passwordentry").set_property('hildon-input-mode', 7|(1 << 29))
116 self._widgetTree.get_widget("callbackcombo").get_child().set_property('hildon-input-mode', (1 << 4))
117 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('recent_scrolledwindow'), True)
118 hildon.hildon_helper_set_thumb_scrollbar(self._widgetTree.get_widget('contacts_scrolledwindow'), True)
120 gtkMenu = self._widgetTree.get_widget("dialpad_menubar")
122 for child in gtkMenu.get_children():
124 self._window.set_menu(menu)
127 self._window.connect("key-press-event", self._on_key_press)
128 self._window.connect("window-state-event", self._on_window_state_change)
130 pass # warnings.warn("No Hildon", UserWarning, 2)
132 if hildon is not None:
133 self._window.set_title("Keypad")
135 self._window.set_title("%s - Keypad" % self.__pretty_app_name__)
138 "on_dialpad_quit": self._on_close,
140 self._widgetTree.signal_autoconnect(callbackMapping)
143 self._window.connect("destroy", gtk.main_quit)
144 self._window.show_all()
145 self._window.set_default_size(800, 300)
147 backgroundSetup = threading.Thread(target=self._idle_setup)
148 backgroundSetup.setDaemon(True)
149 backgroundSetup.start()
151 def _idle_setup(self):
153 If something can be done after the UI loads, push it here so it's not blocking the UI
155 # Barebones UI handlers
159 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
160 with gtk_toolbox.gtk_lock():
161 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
162 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
163 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
164 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
165 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
167 self._dialpads[self._selectedBackendId].enable()
168 self._accountViews[self._selectedBackendId].enable()
169 self._recentViews[self._selectedBackendId].enable()
170 self._messagesViews[self._selectedBackendId].enable()
171 self._contactsViews[self._selectedBackendId].enable()
173 # Setup maemo specifics
180 self._osso = osso.Context(Dialcentral.__app_name__, Dialcentral.__version__, False)
181 device = osso.DeviceState(self._osso)
182 device.set_device_state_callback(self._on_device_state_change, 0)
184 pass # warnings.warn("No OSSO", UserWarning)
190 self._connection = None
191 if conic is not None:
192 self._connection = conic.Connection()
193 self._connection.connect("connection-event", self._on_connection_change, Dialcentral.__app_magic__)
194 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
196 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
198 # Setup costly backends
206 os.makedirs(self._data_path)
210 gcCookiePath = os.path.join(self._data_path, "gc_cookies.txt")
211 gvCookiePath = os.path.join(self._data_path, "gv_cookies.txt")
212 self._defaultBackendId = self._guess_preferred_backend((
213 (self.GC_BACKEND, gcCookiePath),
214 (self.GV_BACKEND, gvCookiePath),
217 self._phoneBackends.update({
218 self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
219 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
221 with gtk_toolbox.gtk_lock():
222 unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
223 unifiedDialpad.set_number("")
224 self._dialpads.update({
225 self.GC_BACKEND: unifiedDialpad,
226 self.GV_BACKEND: unifiedDialpad,
228 self._accountViews.update({
229 self.GC_BACKEND: gc_views.AccountInfo(
230 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
232 self.GV_BACKEND: gc_views.AccountInfo(
233 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
236 self._recentViews.update({
237 self.GC_BACKEND: gc_views.RecentCallsView(
238 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
240 self.GV_BACKEND: gc_views.RecentCallsView(
241 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
244 self._messagesViews.update({
245 self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
246 self.GV_BACKEND: gc_views.MessagesView(
247 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
250 self._contactsViews.update({
251 self.GC_BACKEND: gc_views.ContactsView(
252 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
254 self.GV_BACKEND: gc_views.ContactsView(
255 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
259 evoBackend = evo_backend.EvolutionAddressBook()
260 fsContactsPath = os.path.join(self._data_path, "contacts")
261 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
262 for backendId in (self.GV_BACKEND, self.GC_BACKEND):
263 self._dialpads[backendId].dial = self._on_dial_clicked
264 self._recentViews[backendId].number_selected = self._on_number_selected
265 self._contactsViews[backendId].number_selected = self._on_number_selected
268 self._phoneBackends[backendId],
272 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
273 self._contactsViews[backendId].append(mergedBook)
274 self._contactsViews[backendId].extend(addressBooks)
275 self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
278 "on_paste": self._on_paste,
279 "on_refresh": self._on_refresh,
280 "on_clearcookies_clicked": self._on_clearcookies_clicked,
281 "on_notebook_switch_page": self._on_notebook_switch_page,
282 "on_about_activate": self._on_about_activate,
284 self._widgetTree.signal_autoconnect(callbackMapping)
286 self.attempt_login(2)
290 def attempt_login(self, numOfAttempts = 10):
292 @todo Handle user notification better like attempting to login and failed login
294 @note Not meant to be called directly, but run as a seperate thread.
296 assert 0 < numOfAttempts, "That was pointless having 0 or less login attempts"
298 if not self._deviceIsOnline:
299 warnings.warn("Attempted to login while device was offline")
301 elif self._phoneBackends is None or len(self._phoneBackends) < len(self.BACKENDS):
303 "Attempted to login before initialization is complete, did an event fire early?"
309 if self._phoneBackends[self._defaultBackendId].is_authed():
310 serviceId = self._defaultBackendId
312 for x in xrange(numOfAttempts):
315 with gtk_toolbox.gtk_lock():
316 availableServices = {
317 self.GV_BACKEND: "Google Voice",
318 self.GC_BACKEND: "Grand Central",
320 credentials = self._credentials.request_credentials_from(availableServices)
321 serviceId, username, password = credentials
323 loggedIn = self._phoneBackends[serviceId].login(username, password)
324 except RuntimeError, e:
325 warnings.warn(traceback.format_exc())
326 self._errorDisplay.push_exception_with_lock(e)
328 with gtk_toolbox.gtk_lock():
330 self._errorDisplay.push_message("Login Failed")
331 self._change_loggedin_status(serviceId if loggedIn else self.NULL_BACKEND)
334 def display_error_message(self, msg):
335 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
337 def close(dialog, response, editor):
338 editor.about_dialog = None
340 error_dialog.connect("response", close, self)
343 def _on_close(self, *args, **kwds):
344 if self._osso is not None:
348 def _change_loggedin_status(self, newStatus):
349 oldStatus = self._selectedBackendId
350 if oldStatus == newStatus:
353 self._dialpads[oldStatus].disable()
354 self._accountViews[oldStatus].disable()
355 self._recentViews[oldStatus].disable()
356 self._messagesViews[oldStatus].disable()
357 self._contactsViews[oldStatus].disable()
359 self._dialpads[newStatus].enable()
360 self._accountViews[newStatus].enable()
361 self._recentViews[newStatus].enable()
362 self._messagesViews[newStatus].enable()
363 self._contactsViews[newStatus].enable()
365 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
366 self._phoneBackends[self._selectedBackendId].set_sane_callback()
367 self._accountViews[self._selectedBackendId].update()
369 self._selectedBackendId = newStatus
371 def _guess_preferred_backend(self, backendAndCookiePaths):
373 (getmtime_nothrow(path), backendId, path)
374 for backendId, path in backendAndCookiePaths
376 modTimeAndPath.sort()
377 return modTimeAndPath[-1][1]
379 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
381 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
382 For system_inactivity, we have no background tasks to pause
384 @note Hildon specific
387 for backendId in self.BACKENDS:
388 self._phoneBackends[backendId].clear_caches()
389 self._contactsViews[self._selectedBackendId].clear_caches()
392 def _on_connection_change(self, connection, event, magicIdentifier):
394 @note Hildon specific
398 status = event.get_status()
399 error = event.get_error()
400 iap_id = event.get_iap_id()
401 bearer = event.get_bearer_type()
403 if status == conic.STATUS_CONNECTED:
404 self._deviceIsOnline = True
405 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
406 backgroundLogin.setDaemon(True)
407 backgroundLogin.start()
408 elif status == conic.STATUS_DISCONNECTED:
409 self._deviceIsOnline = False
410 self._defaultBackendId = self._selectedBackendId
411 self._change_loggedin_status(self.NULL_BACKEND)
413 def _on_window_state_change(self, widget, event, *args):
415 @note Hildon specific
417 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
418 self._isFullScreen = True
420 self._isFullScreen = False
422 def _on_key_press(self, widget, event, *args):
424 @note Hildon specific
426 if event.keyval == gtk.keysyms.F6:
427 if self._isFullScreen:
428 self._window.unfullscreen()
430 self._window.fullscreen()
432 def _on_clearcookies_clicked(self, *args):
433 self._phoneBackends[self._selectedBackendId].logout()
434 self._accountViews[self._selectedBackendId].clear()
435 self._recentViews[self._selectedBackendId].clear()
436 self._messagesViews[self._selectedBackendId].clear()
437 self._contactsViews[self._selectedBackendId].clear()
438 self._change_loggedin_status(self.NULL_BACKEND)
440 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
441 backgroundLogin.setDaemon(True)
442 backgroundLogin.start()
444 def _on_notebook_switch_page(self, notebook, page, page_num):
445 if page_num == self.CONTACTS_TAB:
446 self._contactsViews[self._selectedBackendId].update()
447 elif page_num == self.RECENT_TAB:
448 self._recentViews[self._selectedBackendId].update()
449 elif page_num == self.MESSAGES_TAB:
450 self._messagesViews[self._selectedBackendId].update()
452 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
453 if hildon is not None:
454 self._window.set_title(tabTitle)
456 self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
458 def _on_number_selected(self, action, number, message):
459 if action == "select":
460 self._dialpads[self._selectedBackendId].set_number(number)
461 self._notebook.set_current_page(self.KEYPAD_TAB)
462 elif action == "dial":
463 self._on_dial_clicked(number)
464 elif action == "sms":
465 self._on_sms_clicked(number, message)
467 assert False, "Unknown action: %s" % action
469 def _on_sms_clicked(self, number, message):
471 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
476 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
477 except RuntimeError, e:
479 self._errorDisplay.push_exception(e)
483 self._errorDisplay.push_message(
484 "Backend link with grandcentral is not working, please try again"
490 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
492 except RuntimeError, e:
493 self._errorDisplay.push_exception(e)
494 except ValueError, e:
495 self._errorDisplay.push_exception(e)
497 def _on_dial_clicked(self, number):
499 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
503 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
504 except RuntimeError, e:
506 self._errorDisplay.push_exception(e)
510 self._errorDisplay.push_message(
511 "Backend link with grandcentral is not working, please try again"
517 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != ""
518 self._phoneBackends[self._selectedBackendId].dial(number)
520 except RuntimeError, e:
521 self._errorDisplay.push_exception(e)
522 except ValueError, e:
523 self._errorDisplay.push_exception(e)
526 self._dialpads[self._selectedBackendId].clear()
528 def _on_refresh(self, *args):
529 page_num = self._notebook.get_current_page()
530 if page_num == self.CONTACTS_TAB:
531 self._contactsViews[self._selectedBackendId].update(force=True)
532 elif page_num == self.RECENT_TAB:
533 self._recentViews[self._selectedBackendId].update(force=True)
534 elif page_num == self.MESSAGES_TAB:
535 self._messagesViews[self._selectedBackendId].update(force=True)
537 def _on_paste(self, *args):
538 contents = self._clipboard.wait_for_text()
539 self._dialpads[self._selectedBackendId].set_number(contents)
541 def _on_about_activate(self, *args):
542 dlg = gtk.AboutDialog()
543 dlg.set_name(self.__pretty_app_name__)
544 dlg.set_version(self.__version__)
545 dlg.set_copyright("Copyright 2008 - LGPL")
546 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")
547 dlg.set_website("http://gc-dialer.garage.maemo.org/")
548 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
556 failureCount, testCount = doctest.testmod()
558 print "Tests Successful"
565 gtk.gdk.threads_init()
566 if hildon is not None:
567 gtk.set_application_name(Dialcentral.__pretty_app_name__)
568 handle = Dialcentral()
572 class DummyOptions(object):
578 if __name__ == "__main__":
579 if len(sys.argv) > 1:
585 if optparse is not None:
586 parser = optparse.OptionParser()
587 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
588 (commandOptions, commandArgs) = parser.parse_args()
590 commandOptions = DummyOptions()
593 if commandOptions.test: