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
23 from __future__ import with_statement
42 def getmtime_nothrow(path):
44 return os.path.getmtime(path)
49 def display_error_message(msg):
50 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
52 def close(dialog, response):
54 error_dialog.connect("response", close)
58 class Dialcentral(object):
61 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
62 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
63 '/usr/lib/dialcentral/dialcentral.glade',
75 BACKENDS = (NULL_BACKEND, GC_BACKEND, GV_BACKEND)
78 self._initDone = False
79 self._connection = None
81 self._clipboard = gtk.clipboard_get()
83 self._credentials = ("", "")
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
92 self._alarmHandler = None
93 self._ledHandler = None
94 self._originalCurrentLabels = []
96 for path in self._glade_files:
97 if os.path.isfile(path):
98 self._widgetTree = gtk.glade.XML(path)
101 display_error_message("Cannot find dialcentral.glade")
105 self._window = self._widgetTree.get_widget("mainWindow")
106 self._notebook = self._widgetTree.get_widget("notebook")
107 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
108 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
110 self._isFullScreen = False
111 self._app = hildonize.get_app_class()()
112 self._window = hildonize.hildonize_window(self._app, self._window)
113 hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
114 hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
115 hildonize.hildonize_combo_entry(self._widgetTree.get_widget("callbackcombo").get_child())
117 for scrollingWidget in (
118 'recent_scrolledwindow',
119 'message_scrolledwindow',
120 'contacts_scrolledwindow',
121 "phoneSelectionMessage_scrolledwindow",
122 "phonetypes_scrolledwindow",
123 "smsMessage_scrolledwindow",
124 "smsMessage_scrolledEntry",
126 hildonize.set_thumb_scrollbar(self._widgetTree.get_widget(scrollingWidget))
128 hildonize.hildonize_menu(self._window, self._widgetTree.get_widget("dialpad_menubar"))
130 if hildonize.IS_HILDON:
131 self._window.connect("key-press-event", self._on_key_press)
132 self._window.connect("window-state-event", self._on_window_state_change)
134 pass # warnings.warn("No Hildon", UserWarning, 2)
136 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
139 "on_dialpad_quit": self._on_close,
141 self._widgetTree.signal_autoconnect(callbackMapping)
143 self._window.connect("destroy", self._on_close)
144 self._window.set_default_size(800, 300)
145 self._window.show_all()
147 self._loginSink = gtk_toolbox.threaded_stage(
150 gtk_toolbox.null_sink(),
154 backgroundSetup = threading.Thread(target=self._idle_setup)
155 backgroundSetup.setDaemon(True)
156 backgroundSetup.start()
158 def _idle_setup(self):
160 If something can be done after the UI loads, push it here so it's not blocking the UI
163 # Barebones UI handlers
167 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
168 with gtk_toolbox.gtk_lock():
169 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
170 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
171 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
172 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
173 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
175 self._dialpads[self._selectedBackendId].enable()
176 self._accountViews[self._selectedBackendId].enable()
177 self._recentViews[self._selectedBackendId].enable()
178 self._messagesViews[self._selectedBackendId].enable()
179 self._contactsViews[self._selectedBackendId].enable()
181 # Setup maemo specifics
188 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
189 device = osso.DeviceState(self._osso)
190 device.set_device_state_callback(self._on_device_state_change, 0)
192 pass # warnings.warn("No OSSO", UserWarning, 2)
196 self._alarmHandler = alarm_handler.AlarmHandler()
200 with gtk_toolbox.gtk_lock():
201 self._errorDisplay.push_exception()
203 if hildonize.IS_HILDON:
205 self._ledHandler = led_handler.LedHandler()
207 # Setup maemo specifics
212 self._connection = None
213 if conic is not None:
214 self._connection = conic.Connection()
215 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
216 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
218 pass # warnings.warn("No Internet Connectivity API ", UserWarning)
220 # Setup costly backends
227 os.makedirs(constants._data_path_)
231 gcCookiePath = os.path.join(constants._data_path_, "gc_cookies.txt")
232 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
233 self._defaultBackendId = self._guess_preferred_backend((
234 (self.GC_BACKEND, gcCookiePath),
235 (self.GV_BACKEND, gvCookiePath),
238 self._phoneBackends.update({
239 self.GC_BACKEND: gc_backend.GCDialer(gcCookiePath),
240 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
242 with gtk_toolbox.gtk_lock():
243 unifiedDialpad = gc_views.Dialpad(self._widgetTree, self._errorDisplay)
244 unifiedDialpad.set_number("")
245 self._dialpads.update({
246 self.GC_BACKEND: unifiedDialpad,
247 self.GV_BACKEND: unifiedDialpad,
249 self._accountViews.update({
250 self.GC_BACKEND: gc_views.AccountInfo(
251 self._widgetTree, self._phoneBackends[self.GC_BACKEND], None, self._errorDisplay
253 self.GV_BACKEND: gc_views.AccountInfo(
254 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
257 self._accountViews[self.GC_BACKEND].save_everything = lambda *args: None
258 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
259 self._recentViews.update({
260 self.GC_BACKEND: gc_views.RecentCallsView(
261 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
263 self.GV_BACKEND: gc_views.RecentCallsView(
264 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
267 self._messagesViews.update({
268 self.GC_BACKEND: null_views.MessagesView(self._widgetTree),
269 self.GV_BACKEND: gc_views.MessagesView(
270 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
273 self._contactsViews.update({
274 self.GC_BACKEND: gc_views.ContactsView(
275 self._widgetTree, self._phoneBackends[self.GC_BACKEND], self._errorDisplay
277 self.GV_BACKEND: gc_views.ContactsView(
278 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
282 fsContactsPath = os.path.join(constants._data_path_, "contacts")
283 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
284 for backendId in (self.GV_BACKEND, self.GC_BACKEND):
285 self._dialpads[backendId].number_selected = self._select_action
286 self._recentViews[backendId].number_selected = self._select_action
287 self._messagesViews[backendId].number_selected = self._select_action
288 self._contactsViews[backendId].number_selected = self._select_action
291 self._phoneBackends[backendId],
294 mergedBook = gc_views.MergedAddressBook(addressBooks, gc_views.MergedAddressBook.advanced_lastname_sorter)
295 self._contactsViews[backendId].append(mergedBook)
296 self._contactsViews[backendId].extend(addressBooks)
297 self._contactsViews[backendId].open_addressbook(*self._contactsViews[backendId].get_addressbooks().next()[0][0:2])
300 "on_paste": self._on_paste,
301 "on_refresh": self._on_menu_refresh,
302 "on_clearcookies_clicked": self._on_clearcookies_clicked,
303 "on_notebook_switch_page": self._on_notebook_switch_page,
304 "on_about_activate": self._on_about_activate,
306 self._widgetTree.signal_autoconnect(callbackMapping)
308 with gtk_toolbox.gtk_lock():
309 self._originalCurrentLabels = [
310 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
311 for pageIndex in xrange(self._notebook.get_n_pages())
313 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
314 self._notebookTapHandler.enable()
315 self._notebookTapHandler.on_tap = self._reset_tab_refresh
316 self._notebookTapHandler.on_hold = self._on_tab_refresh
317 self._notebookTapHandler.on_holding = self._set_tab_refresh
318 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
320 self._initDone = True
322 config = ConfigParser.SafeConfigParser()
323 config.read(constants._user_settings_)
324 with gtk_toolbox.gtk_lock():
325 self.load_settings(config)
327 self._spawn_attempt_login(2)
329 with gtk_toolbox.gtk_lock():
330 self._errorDisplay.push_exception()
332 def attempt_login(self, numOfAttempts = 10, force = False):
334 @todo Handle user notification better like attempting to login and failed login
336 @note This must be run outside of the UI lock
339 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
340 assert self._initDone, "Attempting login before app is fully loaded"
342 serviceId = self.NULL_BACKEND
346 self.refresh_session()
347 serviceId = self._defaultBackendId
349 except StandardError, e:
350 warnings.warn('Session refresh failed with the following message "%s"' % e.message, UserWarning, 2)
353 loggedIn, serviceId = self._login_by_user(numOfAttempts)
355 with gtk_toolbox.gtk_lock():
356 self._change_loggedin_status(serviceId)
357 except StandardError, e:
358 with gtk_toolbox.gtk_lock():
359 self._errorDisplay.push_exception()
361 def _spawn_attempt_login(self, *args):
362 self._loginSink.send(args)
364 def refresh_session(self):
366 @note Thread agnostic
368 assert self._initDone, "Attempting login before app is fully loaded"
372 loggedIn = self._login_by_cookie()
374 loggedIn = self._login_by_settings()
377 raise RuntimeError("Login Failed")
379 def _login_by_cookie(self):
381 @note Thread agnostic
383 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
386 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
391 def _login_by_settings(self):
393 @note Thread agnostic
395 username, password = self._credentials
396 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
398 self._credentials = username, password
400 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
405 def _login_by_user(self, numOfAttempts):
407 @note This must be run outside of the UI lock
409 loggedIn, (username, password) = False, self._credentials
410 tmpServiceId = self.NULL_BACKEND
411 for attemptCount in xrange(numOfAttempts):
414 availableServices = (
415 (self.GV_BACKEND, "Google Voice"),
416 (self.GC_BACKEND, "Grand Central"),
418 with gtk_toolbox.gtk_lock():
419 credentials = self._credentialsDialog.request_credentials_from(
420 availableServices, defaultCredentials = self._credentials
422 tmpServiceId, username, password = credentials
423 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
426 serviceId = tmpServiceId
427 self._credentials = username, password
429 "Logged into %r through user request" % self._phoneBackends[serviceId],
433 serviceId = self.NULL_BACKEND
435 return loggedIn, serviceId
437 def _select_action(self, action, number, message):
438 self.refresh_session()
439 if action == "select":
440 self._dialpads[self._selectedBackendId].set_number(number)
441 self._notebook.set_current_page(self.KEYPAD_TAB)
442 elif action == "dial":
443 self._on_dial_clicked(number)
444 elif action == "sms":
445 self._on_sms_clicked(number, message)
447 assert False, "Unknown action: %s" % action
449 def _change_loggedin_status(self, newStatus):
450 oldStatus = self._selectedBackendId
451 if oldStatus == newStatus:
454 self._dialpads[oldStatus].disable()
455 self._accountViews[oldStatus].disable()
456 self._recentViews[oldStatus].disable()
457 self._messagesViews[oldStatus].disable()
458 self._contactsViews[oldStatus].disable()
460 self._dialpads[newStatus].enable()
461 self._accountViews[newStatus].enable()
462 self._recentViews[newStatus].enable()
463 self._messagesViews[newStatus].enable()
464 self._contactsViews[newStatus].enable()
466 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
467 self._phoneBackends[self._selectedBackendId].set_sane_callback()
468 self._accountViews[self._selectedBackendId].update()
470 self._selectedBackendId = newStatus
472 def load_settings(self, config):
477 self._defaultBackendId = int(config.get(constants.__pretty_app_name__, "active"))
479 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
480 for i in xrange(len(self._credentials))
483 base64.b64decode(blob)
486 self._credentials = tuple(creds)
488 if self._alarmHandler is not None:
489 self._alarmHandler.load_settings(config, "alarm")
490 except ConfigParser.NoOptionError, e:
492 "Settings file %s is missing section %s" % (
493 constants._user_settings_,
498 except ConfigParser.NoSectionError, e:
500 "Settings file %s is missing section %s" % (
501 constants._user_settings_,
507 for backendId, view in itertools.chain(
508 self._dialpads.iteritems(),
509 self._accountViews.iteritems(),
510 self._messagesViews.iteritems(),
511 self._recentViews.iteritems(),
512 self._contactsViews.iteritems(),
514 sectionName = "%s - %s" % (backendId, view.name())
516 view.load_settings(config, sectionName)
517 except ConfigParser.NoOptionError, e:
519 "Settings file %s is missing section %s" % (
520 constants._user_settings_,
525 except ConfigParser.NoSectionError, e:
527 "Settings file %s is missing section %s" % (
528 constants._user_settings_,
534 def save_settings(self, config):
536 @note Thread Agnostic
538 config.add_section(constants.__pretty_app_name__)
539 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
540 for i, value in enumerate(self._credentials):
541 blob = base64.b64encode(value)
542 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
543 config.add_section("alarm")
544 if self._alarmHandler is not None:
545 self._alarmHandler.save_settings(config, "alarm")
547 for backendId, view in itertools.chain(
548 self._dialpads.iteritems(),
549 self._accountViews.iteritems(),
550 self._messagesViews.iteritems(),
551 self._recentViews.iteritems(),
552 self._contactsViews.iteritems(),
554 sectionName = "%s - %s" % (backendId, view.name())
555 config.add_section(sectionName)
556 view.save_settings(config, sectionName)
558 def _guess_preferred_backend(self, backendAndCookiePaths):
560 (getmtime_nothrow(path), backendId, path)
561 for backendId, path in backendAndCookiePaths
563 modTimeAndPath.sort()
564 return modTimeAndPath[-1][1]
566 def _save_settings(self):
568 @note Thread Agnostic
570 config = ConfigParser.SafeConfigParser()
571 self.save_settings(config)
572 with open(constants._user_settings_, "wb") as configFile:
573 config.write(configFile)
575 def _refresh_active_tab(self):
576 pageIndex = self._notebook.get_current_page()
577 if pageIndex == self.CONTACTS_TAB:
578 self._contactsViews[self._selectedBackendId].update(force=True)
579 elif pageIndex == self.RECENT_TAB:
580 self._recentViews[self._selectedBackendId].update(force=True)
581 elif pageIndex == self.MESSAGES_TAB:
582 self._messagesViews[self._selectedBackendId].update(force=True)
584 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
585 if self._ledHandler is not None:
586 self._ledHandler.off()
588 def _on_close(self, *args, **kwds):
590 if self._osso is not None:
594 self._save_settings()
598 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
600 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
601 For system_inactivity, we have no background tasks to pause
603 @note Hildon specific
606 for backendId in self.BACKENDS:
607 self._phoneBackends[backendId].clear_caches()
608 self._contactsViews[self._selectedBackendId].clear_caches()
611 if save_unsaved_data or shutdown:
612 self._save_settings()
614 def _on_connection_change(self, connection, event, magicIdentifier):
616 @note Hildon specific
620 status = event.get_status()
621 error = event.get_error()
622 iap_id = event.get_iap_id()
623 bearer = event.get_bearer_type()
625 if status == conic.STATUS_CONNECTED:
627 self._spawn_attempt_login(2)
628 elif status == conic.STATUS_DISCONNECTED:
630 self._defaultBackendId = self._selectedBackendId
631 self._change_loggedin_status(self.NULL_BACKEND)
633 def _on_window_state_change(self, widget, event, *args):
635 @note Hildon specific
637 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
638 self._isFullScreen = True
640 self._isFullScreen = False
642 def _on_key_press(self, widget, event, *args):
644 @note Hildon specific
646 if event.keyval == gtk.keysyms.F6:
647 if self._isFullScreen:
648 self._window.unfullscreen()
650 self._window.fullscreen()
652 def _on_clearcookies_clicked(self, *args):
653 self._phoneBackends[self._selectedBackendId].logout()
654 self._accountViews[self._selectedBackendId].clear()
655 self._recentViews[self._selectedBackendId].clear()
656 self._messagesViews[self._selectedBackendId].clear()
657 self._contactsViews[self._selectedBackendId].clear()
658 self._change_loggedin_status(self.NULL_BACKEND)
660 self._spawn_attempt_login(2, True)
662 def _on_notebook_switch_page(self, notebook, page, pageIndex):
663 self._reset_tab_refresh()
665 didRecentUpdate = False
666 didMessagesUpdate = False
668 if pageIndex == self.RECENT_TAB:
669 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
670 elif pageIndex == self.MESSAGES_TAB:
671 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
672 elif pageIndex == self.CONTACTS_TAB:
673 self._contactsViews[self._selectedBackendId].update()
674 elif pageIndex == self.ACCOUNT_TAB:
675 self._accountViews[self._selectedBackendId].update()
677 if didRecentUpdate or didMessagesUpdate:
678 if self._ledHandler is not None:
679 self._ledHandler.off()
681 def _set_tab_refresh(self, *args):
682 pageIndex = self._notebook.get_current_page()
683 child = self._notebook.get_nth_page(pageIndex)
684 self._notebook.get_tab_label(child).set_text("Refresh?")
687 def _reset_tab_refresh(self, *args):
688 pageIndex = self._notebook.get_current_page()
689 child = self._notebook.get_nth_page(pageIndex)
690 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
693 def _on_tab_refresh(self, *args):
694 self._refresh_active_tab()
695 self._reset_tab_refresh()
698 def _on_sms_clicked(self, number, message):
699 assert number, "No number specified"
700 assert message, "Empty message"
702 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
703 except StandardError, e:
705 self._errorDisplay.push_exception()
709 self._errorDisplay.push_message(
710 "Backend link with grandcentral is not working, please try again"
716 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
718 except StandardError, e:
719 self._errorDisplay.push_exception()
720 except ValueError, e:
721 self._errorDisplay.push_exception()
724 self._dialpads[self._selectedBackendId].clear()
726 def _on_dial_clicked(self, number):
727 assert number, "No number to call"
729 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
730 except StandardError, e:
732 self._errorDisplay.push_exception()
736 self._errorDisplay.push_message(
737 "Backend link with grandcentral is not working, please try again"
743 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
744 self._phoneBackends[self._selectedBackendId].dial(number)
746 except StandardError, e:
747 self._errorDisplay.push_exception()
748 except ValueError, e:
749 self._errorDisplay.push_exception()
752 self._dialpads[self._selectedBackendId].clear()
754 def _on_menu_refresh(self, *args):
755 self._refresh_active_tab()
757 def _on_paste(self, *args):
758 contents = self._clipboard.wait_for_text()
759 self._dialpads[self._selectedBackendId].set_number(contents)
761 def _on_about_activate(self, *args):
762 dlg = gtk.AboutDialog()
763 dlg.set_name(constants.__pretty_app_name__)
764 dlg.set_version(constants.__version__)
765 dlg.set_copyright("Copyright 2008 - LGPL")
766 dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice/Grandcentral account. This application is not affiliated with Google in any way")
767 dlg.set_website("http://gc-dialer.garage.maemo.org/")
768 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
776 failureCount, testCount = doctest.testmod()
778 print "Tests Successful"
785 _lock_file = os.path.join(constants._data_path_, ".lock")
786 #with gtk_toolbox.flock(_lock_file, 0):
787 gtk.gdk.threads_init()
789 if hildonize.IS_HILDON:
790 gtk.set_application_name(constants.__pretty_app_name__)
791 handle = Dialcentral()
795 class DummyOptions(object):
801 if __name__ == "__main__":
802 if len(sys.argv) > 1:
808 if optparse is not None:
809 parser = optparse.OptionParser()
810 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
811 (commandOptions, commandArgs) = parser.parse_args()
813 commandOptions = DummyOptions()
816 if commandOptions.test: