diff options
Diffstat (limited to 'src/jarabe/model')
-rw-r--r-- | src/jarabe/model/Makefile.am | 20 | ||||
-rw-r--r-- | src/jarabe/model/Makefile.in | 457 | ||||
-rw-r--r-- | src/jarabe/model/__init__.py | 15 | ||||
-rw-r--r-- | src/jarabe/model/adhoc.py | 282 | ||||
-rw-r--r-- | src/jarabe/model/buddy.py | 213 | ||||
-rw-r--r-- | src/jarabe/model/bundleregistry.py | 450 | ||||
-rw-r--r-- | src/jarabe/model/filetransfer.py | 368 | ||||
-rw-r--r-- | src/jarabe/model/friends.py | 174 | ||||
-rw-r--r-- | src/jarabe/model/invites.py | 289 | ||||
-rw-r--r-- | src/jarabe/model/mimeregistry.py | 50 | ||||
-rw-r--r-- | src/jarabe/model/neighborhood.py | 1084 | ||||
-rw-r--r-- | src/jarabe/model/network.py | 1096 | ||||
-rw-r--r-- | src/jarabe/model/notifications.py | 98 | ||||
-rw-r--r-- | src/jarabe/model/olpcmesh.py | 228 | ||||
-rw-r--r-- | src/jarabe/model/screen.py | 45 | ||||
-rw-r--r-- | src/jarabe/model/session.py | 113 | ||||
-rw-r--r-- | src/jarabe/model/shell.py | 675 | ||||
-rw-r--r-- | src/jarabe/model/sound.py | 65 | ||||
-rw-r--r-- | src/jarabe/model/speech.py | 232 | ||||
-rw-r--r-- | src/jarabe/model/telepathyclient.py | 126 |
20 files changed, 6080 insertions, 0 deletions
diff --git a/src/jarabe/model/Makefile.am b/src/jarabe/model/Makefile.am new file mode 100644 index 0000000..2fc6b1c --- /dev/null +++ b/src/jarabe/model/Makefile.am @@ -0,0 +1,20 @@ +sugardir = $(pythondir)/jarabe/model +sugar_PYTHON = \ + adhoc.py \ + __init__.py \ + buddy.py \ + bundleregistry.py \ + filetransfer.py \ + friends.py \ + invites.py \ + olpcmesh.py \ + mimeregistry.py \ + neighborhood.py \ + network.py \ + notifications.py \ + shell.py \ + screen.py \ + session.py \ + sound.py \ + speech.py \ + telepathyclient.py diff --git a/src/jarabe/model/Makefile.in b/src/jarabe/model/Makefile.in new file mode 100644 index 0000000..f76fc87 --- /dev/null +++ b/src/jarabe/model/Makefile.in @@ -0,0 +1,457 @@ +# Makefile.in generated by automake 1.11.3 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, +# 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Free Software +# Foundation, Inc. +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ +VPATH = @srcdir@ +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +subdir = src/jarabe/model +DIST_COMMON = $(srcdir)/Makefile.am $(srcdir)/Makefile.in \ + $(sugar_PYTHON) +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +mkinstalldirs = $(install_sh) -d +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +SOURCES = +DIST_SOURCES = +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; +am__install_max = 40 +am__nobase_strip_setup = \ + srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` +am__nobase_strip = \ + for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" +am__nobase_list = $(am__nobase_strip_setup); \ + for p in $$list; do echo "$$p $$p"; done | \ + sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ + $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ + if (++n[$$2] == $(am__install_max)) \ + { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ + END { for (dir in files) print dir, files[dir] }' +am__base_list = \ + sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ + sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' +am__uninstall_files_from_dir = { \ + test -z "$$files" \ + || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ + || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ + $(am__cd) "$$dir" && rm -f $$files; }; \ + } +am__py_compile = PYTHON=$(PYTHON) $(SHELL) $(py_compile) +am__installdirs = "$(DESTDIR)$(sugardir)" +py_compile = $(top_srcdir)/py-compile +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +ALL_LINGUAS = @ALL_LINGUAS@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +CATALOGS = @CATALOGS@ +CATOBJEXT = @CATOBJEXT@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CFLAGS = @CFLAGS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DATADIRNAME = @DATADIRNAME@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +EXEEXT = @EXEEXT@ +GCONFTOOL = @GCONFTOOL@ +GCONF_SCHEMA_CONFIG_SOURCE = @GCONF_SCHEMA_CONFIG_SOURCE@ +GCONF_SCHEMA_FILE_DIR = @GCONF_SCHEMA_FILE_DIR@ +GETTEXT_PACKAGE = @GETTEXT_PACKAGE@ +GMOFILES = @GMOFILES@ +GMSGFMT = @GMSGFMT@ +GREP = @GREP@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +INSTOBJEXT = @INSTOBJEXT@ +INTLLIBS = @INTLLIBS@ +INTLTOOL_EXTRACT = @INTLTOOL_EXTRACT@ +INTLTOOL_MERGE = @INTLTOOL_MERGE@ +INTLTOOL_PERL = @INTLTOOL_PERL@ +INTLTOOL_UPDATE = @INTLTOOL_UPDATE@ +INTLTOOL_V_MERGE = @INTLTOOL_V_MERGE@ +INTLTOOL_V_MERGE_OPTIONS = @INTLTOOL_V_MERGE_OPTIONS@ +INTLTOOL__v_MERGE_ = @INTLTOOL__v_MERGE_@ +INTLTOOL__v_MERGE_0 = @INTLTOOL__v_MERGE_0@ +LDFLAGS = @LDFLAGS@ +LIBOBJS = @LIBOBJS@ +LIBS = @LIBS@ +LTLIBOBJS = @LTLIBOBJS@ +MAINT = @MAINT@ +MAKEINFO = @MAKEINFO@ +MKDIR_P = @MKDIR_P@ +MKINSTALLDIRS = @MKINSTALLDIRS@ +MSGFMT = @MSGFMT@ +MSGFMT_OPTS = @MSGFMT_OPTS@ +MSGMERGE = @MSGMERGE@ +OBJEXT = @OBJEXT@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PKG_CONFIG = @PKG_CONFIG@ +PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@ +PKG_CONFIG_PATH = @PKG_CONFIG_PATH@ +POFILES = @POFILES@ +POSUB = @POSUB@ +PO_IN_DATADIR_FALSE = @PO_IN_DATADIR_FALSE@ +PO_IN_DATADIR_TRUE = @PO_IN_DATADIR_TRUE@ +PYTHON = @PYTHON@ +PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@ +PYTHON_PLATFORM = @PYTHON_PLATFORM@ +PYTHON_PREFIX = @PYTHON_PREFIX@ +PYTHON_VERSION = @PYTHON_VERSION@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +SHELL_CFLAGS = @SHELL_CFLAGS@ +SHELL_LIBS = @SHELL_LIBS@ +STRIP = @STRIP@ +SUCROSE_VERSION = @SUCROSE_VERSION@ +USE_NLS = @USE_NLS@ +VERSION = @VERSION@ +XGETTEXT = @XGETTEXT@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_CC = @ac_ct_CC@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build_alias = @build_alias@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +exec_prefix = @exec_prefix@ +host_alias = @host_alias@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +intltool__v_merge_options_ = @intltool__v_merge_options_@ +intltool__v_merge_options_0 = @intltool__v_merge_options_0@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +pkgpyexecdir = @pkgpyexecdir@ +pkgpythondir = @pkgpythondir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +pyexecdir = @pyexecdir@ +pythondir = @pythondir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +srcdir = @srcdir@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +sugardir = $(pythondir)/jarabe/model +sugar_PYTHON = \ + adhoc.py \ + __init__.py \ + buddy.py \ + bundleregistry.py \ + filetransfer.py \ + friends.py \ + invites.py \ + olpcmesh.py \ + mimeregistry.py \ + neighborhood.py \ + network.py \ + notifications.py \ + shell.py \ + screen.py \ + session.py \ + sound.py \ + speech.py \ + telepathyclient.py + +all: all-am + +.SUFFIXES: +$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/jarabe/model/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/jarabe/model/Makefile +.PRECIOUS: Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__depfiles_maybe);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): +install-sugarPYTHON: $(sugar_PYTHON) + @$(NORMAL_INSTALL) + test -z "$(sugardir)" || $(MKDIR_P) "$(DESTDIR)$(sugardir)" + @list='$(sugar_PYTHON)'; dlist=; list2=; test -n "$(sugardir)" || list=; \ + for p in $$list; do \ + if test -f "$$p"; then b=; else b="$(srcdir)/"; fi; \ + if test -f $$b$$p; then \ + $(am__strip_dir) \ + dlist="$$dlist $$f"; \ + list2="$$list2 $$b$$p"; \ + else :; fi; \ + done; \ + for file in $$list2; do echo $$file; done | $(am__base_list) | \ + while read files; do \ + echo " $(INSTALL_DATA) $$files '$(DESTDIR)$(sugardir)'"; \ + $(INSTALL_DATA) $$files "$(DESTDIR)$(sugardir)" || exit $$?; \ + done || exit $$?; \ + if test -n "$$dlist"; then \ + $(am__py_compile) --destdir "$(DESTDIR)" \ + --basedir "$(sugardir)" $$dlist; \ + else :; fi + +uninstall-sugarPYTHON: + @$(NORMAL_UNINSTALL) + @list='$(sugar_PYTHON)'; test -n "$(sugardir)" || list=; \ + files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \ + test -n "$$files" || exit 0; \ + dir='$(DESTDIR)$(sugardir)'; \ + filesc=`echo "$$files" | sed 's|$$|c|'`; \ + fileso=`echo "$$files" | sed 's|$$|o|'`; \ + st=0; \ + for files in "$$files" "$$filesc" "$$fileso"; do \ + $(am__uninstall_files_from_dir) || st=$$?; \ + done; \ + exit $$st +tags: TAGS +TAGS: + +ctags: CTAGS +CTAGS: + + +distdir: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am +check: check-am +all-am: Makefile +installdirs: + for dir in "$(DESTDIR)$(sugardir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-am + +clean-am: clean-generic mostlyclean-am + +distclean: distclean-am + -rm -f Makefile +distclean-am: clean-am distclean-generic + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: install-sugarPYTHON + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-generic + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: uninstall-sugarPYTHON + +.MAKE: install-am install-strip + +.PHONY: all all-am check check-am clean clean-generic distclean \ + distclean-generic distdir dvi dvi-am html html-am info info-am \ + install install-am install-data install-data-am install-dvi \ + install-dvi-am install-exec install-exec-am install-html \ + install-html-am install-info install-info-am install-man \ + install-pdf install-pdf-am install-ps install-ps-am \ + install-strip install-sugarPYTHON installcheck installcheck-am \ + installdirs maintainer-clean maintainer-clean-generic \ + mostlyclean mostlyclean-generic pdf pdf-am ps ps-am uninstall \ + uninstall-am uninstall-sugarPYTHON + + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/src/jarabe/model/__init__.py b/src/jarabe/model/__init__.py new file mode 100644 index 0000000..85f6a24 --- /dev/null +++ b/src/jarabe/model/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 2006-2007, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/src/jarabe/model/adhoc.py b/src/jarabe/model/adhoc.py new file mode 100644 index 0000000..68a9aa3 --- /dev/null +++ b/src/jarabe/model/adhoc.py @@ -0,0 +1,282 @@ +# Copyright (C) 2010 One Laptop per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +import gobject + +from jarabe.model import network +from jarabe.model.network import Settings +from sugar.util import unique_id +from jarabe.model.network import IP4Config + + +_adhoc_manager_instance = None + + +def get_adhoc_manager_instance(): + global _adhoc_manager_instance + if _adhoc_manager_instance is None: + _adhoc_manager_instance = AdHocManager() + return _adhoc_manager_instance + + +class AdHocManager(gobject.GObject): + """To mimic the mesh behavior on devices where mesh hardware is + not available we support the creation of an Ad-hoc network on + three channels 1, 6, 11. If Sugar sees no "known" network when it + starts, it does autoconnect to an Ad-hoc network. + + """ + + __gsignals__ = { + 'members-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + 'state-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])), + } + + _AUTOCONNECT_TIMEOUT = 60 + _CHANNEL_1 = 1 + _CHANNEL_6 = 6 + _CHANNEL_11 = 11 + + def __init__(self): + gobject.GObject.__init__(self) + + self._bus = dbus.SystemBus() + self._device = None + self._idle_source = 0 + self._listening_called = 0 + self._device_state = network.NM_DEVICE_STATE_UNKNOWN + + self._current_channel = None + self._networks = {self._CHANNEL_1: None, + self._CHANNEL_6: None, + self._CHANNEL_11: None} + + for channel in (self._CHANNEL_1, self._CHANNEL_6, self._CHANNEL_11): + if not self._find_connection(channel): + self._add_connection(channel) + + def start_listening(self, device): + self._listening_called += 1 + if self._listening_called > 1: + raise RuntimeError('The start listening method can' \ + ' only be called once.') + + self._device = device + props = dbus.Interface(device, dbus.PROPERTIES_IFACE) + self._device_state = props.Get(network.NM_DEVICE_IFACE, 'State') + + self._bus.add_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=network.NM_DEVICE_IFACE) + + self._bus.add_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._device.object_path, + dbus_interface=network.NM_WIRELESS_IFACE) + + def stop_listening(self): + self._listening_called = 0 + self._bus.remove_signal_receiver(self.__device_state_changed_cb, + signal_name='StateChanged', + path=self._device.object_path, + dbus_interface=network.NM_DEVICE_IFACE) + self._bus.remove_signal_receiver(self.__wireless_properties_changed_cb, + signal_name='PropertiesChanged', + path=self._device.object_path, + dbus_interface=network.NM_WIRELESS_IFACE) + + def __device_state_changed_cb(self, new_state, old_state, reason): + self._device_state = new_state + self._update_state() + + def __wireless_properties_changed_cb(self, properties): + if 'ActiveAccessPoint' in properties and \ + properties['ActiveAccessPoint'] != '/': + active_ap = self._bus.get_object(network.NM_SERVICE, + properties['ActiveAccessPoint']) + props = dbus.Interface(active_ap, dbus.PROPERTIES_IFACE) + props.GetAll(network.NM_ACCESSPOINT_IFACE, byte_arrays=True, + reply_handler=self.__get_all_ap_props_reply_cb, + error_handler=self.__get_all_ap_props_error_cb) + + def __get_all_ap_props_reply_cb(self, properties): + if properties['Mode'] == network.NM_802_11_MODE_ADHOC and \ + 'Frequency' in properties: + frequency = properties['Frequency'] + self._current_channel = network.frequency_to_channel(frequency) + else: + self._current_channel = None + self._update_state() + + def __get_all_ap_props_error_cb(self, err): + logging.error('Error getting the access point properties: %s', err) + + def _update_state(self): + self.emit('state-changed', self._current_channel, self._device_state) + + def autoconnect(self): + """Start a timer which basically looks for 30 seconds of inactivity + on the device, then does autoconnect to an Ad-hoc network. + + This function may be called early on (e.g. when the device is still + in NM_DEVICE_STATE_UNMANAGED). It is assumed that initialisation + will complete quickly, and long before the timeout ticks. + """ + if self._idle_source != 0: + gobject.source_remove(self._idle_source) + self._idle_source = gobject.timeout_add_seconds( + self._AUTOCONNECT_TIMEOUT, self.__idle_check_cb) + + def __idle_check_cb(self): + if self._device_state == network.NM_DEVICE_STATE_DISCONNECTED: + logging.debug('Connect to Ad-hoc network due to inactivity.') + self._autoconnect_adhoc() + else: + logging.debug('autoconnect Sugar Ad-hoc: already connected') + return False + + def _autoconnect_adhoc(self): + """First we try if there is an Ad-hoc network that is used by other + learners in the area, if not we default to channel 1. + + """ + if self._networks[self._CHANNEL_1] is not None: + self.activate_channel(self._CHANNEL_1) + elif self._networks[self._CHANNEL_6] is not None: + self.activate_channel(self._CHANNEL_6) + elif self._networks[self._CHANNEL_11] is not None: + self.activate_channel(self._CHANNEL_11) + else: + self.activate_channel(self._CHANNEL_1) + + def activate_channel(self, channel): + """Activate a sugar Ad-hoc network. + + Keyword arguments: + channel -- Channel to connect to (should be 1, 6, 11) + + """ + connection = self._find_connection(channel) + if connection: + connection.activate(self._device.object_path) + + @staticmethod + def _get_connection_id(channel): + return '%s%d' % (network.ADHOC_CONNECTION_ID_PREFIX, channel) + + def _add_connection(self, channel): + ssid = 'Ad-hoc Network %d' % (channel,) + settings = Settings() + settings.connection.id = self._get_connection_id(channel) + settings.connection.uuid = unique_id() + settings.connection.type = '802-11-wireless' + settings.connection.autoconnect = False + settings.wireless.ssid = dbus.ByteArray(ssid) + settings.wireless.band = 'bg' + settings.wireless.channel = channel + settings.wireless.mode = 'adhoc' + settings.ip4_config = IP4Config() + settings.ip4_config.method = 'link-local' + network.add_connection(settings) + + def _find_connection(self, channel): + connection_id = self._get_connection_id(channel) + return network.find_connection_by_id(connection_id) + + def deactivate_active_channel(self): + """Deactivate the current active channel.""" + obj = self._bus.get_object(network.NM_SERVICE, network.NM_PATH) + netmgr = dbus.Interface(obj, network.NM_IFACE) + + netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE) + netmgr_props.Get(network.NM_IFACE, 'ActiveConnections', \ + reply_handler=self.__get_active_connections_reply_cb, + error_handler=self.__get_active_connections_error_cb) + + def __get_active_connections_reply_cb(self, active_connections_o): + for connection_o in active_connections_o: + obj = self._bus.get_object(network.NM_IFACE, connection_o) + props = dbus.Interface(obj, dbus.PROPERTIES_IFACE) + state = props.Get(network.NM_ACTIVE_CONN_IFACE, 'State') + if state == network.NM_ACTIVE_CONNECTION_STATE_ACTIVATED: + access_point_o = props.Get(network.NM_ACTIVE_CONN_IFACE, + 'SpecificObject') + if access_point_o != '/': + obj = self._bus.get_object(network.NM_SERVICE, network.NM_PATH) + netmgr = dbus.Interface(obj, network.NM_IFACE) + netmgr.DeactivateConnection(connection_o) + + def __get_active_connections_error_cb(self, err): + logging.error('Error getting the active connections: %s', err) + + def __activate_reply_cb(self, connection): + logging.debug('Ad-hoc network created: %s', connection) + + def __activate_error_cb(self, err): + logging.error('Failed to create Ad-hoc network: %s', err) + + def add_access_point(self, access_point): + """Add an access point to a network and notify the view to idicate + the member change. + + Keyword arguments: + access_point -- Access Point + + """ + if access_point.ssid.endswith(' 1'): + self._networks[self._CHANNEL_1] = access_point + self.emit('members-changed', self._CHANNEL_1, True) + elif access_point.ssid.endswith(' 6'): + self._networks[self._CHANNEL_6] = access_point + self.emit('members-changed', self._CHANNEL_6, True) + elif access_point.ssid.endswith('11'): + self._networks[self._CHANNEL_11] = access_point + self.emit('members-changed', self._CHANNEL_11, True) + + def is_sugar_adhoc_access_point(self, ap_object_path): + """Checks whether an access point is part of a sugar Ad-hoc network. + + Keyword arguments: + ap_object_path -- Access Point object path + + Return: Boolean + + """ + for access_point in self._networks.values(): + if access_point is not None: + if access_point.model.object_path == ap_object_path: + return True + return False + + def remove_access_point(self, ap_object_path): + """Remove an access point from a sugar Ad-hoc network. + + Keyword arguments: + ap_object_path -- Access Point object path + + """ + for channel in self._networks: + if self._networks[channel] is not None: + if self._networks[channel].model.object_path == ap_object_path: + self.emit('members-changed', channel, False) + self._networks[channel] = None + break diff --git a/src/jarabe/model/buddy.py b/src/jarabe/model/buddy.py new file mode 100644 index 0000000..8f17d7e --- /dev/null +++ b/src/jarabe/model/buddy.py @@ -0,0 +1,213 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import gobject +import gconf +import dbus +from telepathy.client import Connection +from telepathy.interfaces import CONNECTION + +from sugar.graphics.xocolor import XoColor +from sugar.profile import get_profile + +from jarabe.util.telepathy import connection_watcher + + +CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' + +_owner_instance = None + + +class BaseBuddyModel(gobject.GObject): + __gtype_name__ = 'SugarBaseBuddyModel' + + def __init__(self, **kwargs): + self._key = None + self._nick = None + self._color = None + self._tags = None + self._current_activity = None + + gobject.GObject.__init__(self, **kwargs) + + def get_nick(self): + return self._nick + + def set_nick(self, nick): + self._nick = nick + + nick = gobject.property(type=object, getter=get_nick, setter=set_nick) + + def get_key(self): + return self._key + + def set_key(self, key): + self._key = key + + key = gobject.property(type=object, getter=get_key, setter=set_key) + + def get_color(self): + return self._color + + def set_color(self, color): + self._color = color + + color = gobject.property(type=object, getter=get_color, setter=set_color) + + def get_tags(self): + return self._tags + + tags = gobject.property(type=object, getter=get_tags) + + def get_current_activity(self): + return self._current_activity + + def set_current_activity(self, current_activity): + if self._current_activity != current_activity: + self._current_activity = current_activity + self.notify('current-activity') + + current_activity = gobject.property(type=object, + getter=get_current_activity, + setter=set_current_activity) + + def is_owner(self): + raise NotImplementedError + + +class OwnerBuddyModel(BaseBuddyModel): + __gtype_name__ = 'SugarOwnerBuddyModel' + + def __init__(self): + BaseBuddyModel.__init__(self) + + client = gconf.client_get_default() + self.props.nick = client.get_string('/desktop/sugar/user/nick') + color = client.get_string('/desktop/sugar/user/color') + self.props.color = XoColor(color) + + self.props.key = get_profile().pubkey + + self.connect('notify::nick', self.__property_changed_cb) + self.connect('notify::color', self.__property_changed_cb) + + bus = dbus.SessionBus() + bus.add_signal_receiver( + self.__name_owner_changed_cb, + signal_name='NameOwnerChanged', + dbus_interface='org.freedesktop.DBus') + + bus_object = bus.get_object(dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH) + for service in bus_object.ListNames( + dbus_interface=dbus.BUS_DAEMON_IFACE): + if service.startswith(CONNECTION + '.'): + path = '/%s' % service.replace('.', '/') + Connection(service, path, bus, + ready_handler=self.__connection_ready_cb) + + def __connection_ready_cb(self, connection): + self._sync_properties_on_connection(connection) + + def __name_owner_changed_cb(self, name, old, new): + if name.startswith(CONNECTION + '.') and not old and new: + path = '/' + name.replace('.', '/') + Connection(name, path, ready_handler=self.__connection_ready_cb) + + def __property_changed_cb(self, buddy, pspec): + self._sync_properties() + + def _sync_properties(self): + conn_watcher = connection_watcher.get_instance() + for connection in conn_watcher.get_connections(): + self._sync_properties_on_connection(connection) + + def _sync_properties_on_connection(self, connection): + if CONNECTION_INTERFACE_BUDDY_INFO in connection: + properties = {} + if self.props.key is not None: + properties['key'] = dbus.ByteArray(self.props.key) + if self.props.color is not None: + properties['color'] = self.props.color.to_string() + + logging.debug('calling SetProperties with %r', properties) + connection[CONNECTION_INTERFACE_BUDDY_INFO].SetProperties( + properties, + reply_handler=self.__set_properties_cb, + error_handler=self.__error_handler_cb) + + def __set_properties_cb(self): + logging.debug('__set_properties_cb') + + def __error_handler_cb(self, error): + raise RuntimeError(error) + + def __connection_added_cb(self, conn_watcher, connection): + self._sync_properties_on_connection(connection) + + def is_owner(self): + return True + + +def get_owner_instance(): + global _owner_instance + if _owner_instance is None: + _owner_instance = OwnerBuddyModel() + return _owner_instance + + +class BuddyModel(BaseBuddyModel): + __gtype_name__ = 'SugarBuddyModel' + + def __init__(self, **kwargs): + + self._account = None + self._contact_id = None + self._handle = None + + BaseBuddyModel.__init__(self, **kwargs) + + def is_owner(self): + return False + + def get_account(self): + return self._account + + def set_account(self, account): + self._account = account + + account = gobject.property(type=object, getter=get_account, + setter=set_account) + + def get_contact_id(self): + return self._contact_id + + def set_contact_id(self, contact_id): + self._contact_id = contact_id + + contact_id = gobject.property(type=object, getter=get_contact_id, + setter=set_contact_id) + + def get_handle(self): + return self._handle + + def set_handle(self, handle): + self._handle = handle + + handle = gobject.property(type=object, getter=get_handle, + setter=set_handle) diff --git a/src/jarabe/model/bundleregistry.py b/src/jarabe/model/bundleregistry.py new file mode 100644 index 0000000..26e719f --- /dev/null +++ b/src/jarabe/model/bundleregistry.py @@ -0,0 +1,450 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2009 Aleksey Lim +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging + +import gconf +import gobject +import gio +import simplejson + +from sugar.bundle.activitybundle import ActivityBundle +from sugar.bundle.contentbundle import ContentBundle +from sugar.bundle.bundleversion import NormalizedVersion +from jarabe.journal.journalentrybundle import JournalEntryBundle +from sugar.bundle.bundle import MalformedBundleException, \ + AlreadyInstalledException, RegistrationException +from sugar import env + +from jarabe import config +from jarabe.model import mimeregistry + + +_instance = None + + +class BundleRegistry(gobject.GObject): + """Tracks the available activity bundles""" + + __gsignals__ = { + 'bundle-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'bundle-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'bundle-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + } + + def __init__(self): + logging.debug('STARTUP: Loading the bundle registry') + gobject.GObject.__init__(self) + + self._mime_defaults = self._load_mime_defaults() + + self._bundles = [] + # hold a reference to the monitors so they don't get disposed + self._gio_monitors = [] + + user_path = env.get_user_activities_path() + for activity_dir in [user_path, config.activities_path]: + self._scan_directory(activity_dir) + directory = gio.File(activity_dir) + monitor = directory.monitor_directory() + monitor.connect('changed', self.__file_monitor_changed_cb) + self._gio_monitors.append(monitor) + + self._last_defaults_mtime = -1 + self._favorite_bundles = {} + + client = gconf.client_get_default() + self._protected_activities = client.get_list( + '/desktop/sugar/protected_activities', + gconf.VALUE_STRING) + + if self._protected_activities is None: + self._protected_activities = [] + + try: + self._load_favorites() + except Exception: + logging.exception('Error while loading favorite_activities.') + + self._merge_default_favorites() + + def __file_monitor_changed_cb(self, monitor, one_file, other_file, + event_type): + if not one_file.get_path().endswith('.activity'): + return + if event_type == gio.FILE_MONITOR_EVENT_CREATED: + self.add_bundle(one_file.get_path(), install_mime_type=True) + elif event_type == gio.FILE_MONITOR_EVENT_DELETED: + self.remove_bundle(one_file.get_path()) + + def _load_mime_defaults(self): + defaults = {} + + f = open(os.path.join(config.data_path, 'mime.defaults'), 'r') + for line in f.readlines(): + line = line.strip() + if line and not line.startswith('#'): + mime = line[:line.find(' ')] + handler = line[line.rfind(' ') + 1:] + defaults[mime] = handler + f.close() + + return defaults + + def _get_favorite_key(self, bundle_id, version): + """We use a string as a composite key for the favorites dictionary + because JSON doesn't support tuples and python won't accept a list + as a dictionary key. + """ + if ' ' in bundle_id: + raise ValueError('bundle_id cannot contain spaces') + return '%s %s' % (bundle_id, version) + + def _load_favorites(self): + favorites_path = env.get_profile_path('favorite_activities') + if os.path.exists(favorites_path): + favorites_data = simplejson.load(open(favorites_path)) + + favorite_bundles = favorites_data['favorites'] + if not isinstance(favorite_bundles, dict): + raise ValueError('Invalid format in %s.' % favorites_path) + if favorite_bundles: + first_key = favorite_bundles.keys()[0] + if not isinstance(first_key, basestring): + raise ValueError('Invalid format in %s.' % favorites_path) + + first_value = favorite_bundles.values()[0] + if first_value is not None and \ + not isinstance(first_value, dict): + raise ValueError('Invalid format in %s.' % favorites_path) + + self._last_defaults_mtime = float(favorites_data['defaults-mtime']) + self._favorite_bundles = favorite_bundles + + def _merge_default_favorites(self): + default_activities = [] + defaults_path = os.path.join(config.data_path, 'activities.defaults') + if os.path.exists(defaults_path): + file_mtime = os.stat(defaults_path).st_mtime + if file_mtime > self._last_defaults_mtime: + f = open(defaults_path, 'r') + for line in f.readlines(): + line = line.strip() + if line and not line.startswith('#'): + default_activities.append(line) + f.close() + self._last_defaults_mtime = file_mtime + + if not default_activities: + return + + for bundle_id in default_activities: + max_version = '0' + for bundle in self._bundles: + if bundle.get_bundle_id() == bundle_id and \ + NormalizedVersion(max_version) < \ + NormalizedVersion(bundle.get_activity_version()): + max_version = bundle.get_activity_version() + + key = self._get_favorite_key(bundle_id, max_version) + if NormalizedVersion(max_version) > NormalizedVersion('0') and \ + key not in self._favorite_bundles: + self._favorite_bundles[key] = None + + logging.debug('After merging: %r', self._favorite_bundles) + + self._write_favorites_file() + + def get_bundle(self, bundle_id): + """Returns an bundle given his service name""" + for bundle in self._bundles: + if bundle.get_bundle_id() == bundle_id: + return bundle + return None + + def __iter__(self): + return self._bundles.__iter__() + + def __len__(self): + return len(self._bundles) + + def _scan_directory(self, path): + if not os.path.isdir(path): + return + + # Sort by mtime to ensure a stable activity order + bundles = {} + for f in os.listdir(path): + if not f.endswith('.activity'): + continue + try: + bundle_dir = os.path.join(path, f) + if os.path.isdir(bundle_dir): + bundles[bundle_dir] = os.stat(bundle_dir).st_mtime + except Exception: + logging.exception('Error while processing installed activity' + ' bundle %s:', bundle_dir) + + bundle_dirs = bundles.keys() + bundle_dirs.sort(lambda d1, d2: cmp(bundles[d1], bundles[d2])) + for folder in bundle_dirs: + try: + self._add_bundle(folder) + except: + # pylint: disable=W0702 + logging.exception('Error while processing installed activity' + ' bundle %s:', folder) + + def add_bundle(self, bundle_path, install_mime_type=False): + bundle = self._add_bundle(bundle_path, install_mime_type) + if bundle is not None: + self._set_bundle_favorite(bundle.get_bundle_id(), + bundle.get_activity_version(), + True) + self.emit('bundle-added', bundle) + return True + else: + return False + + def _add_bundle(self, bundle_path, install_mime_type=False): + logging.debug('STARTUP: Adding bundle %r', bundle_path) + try: + bundle = ActivityBundle(bundle_path) + if install_mime_type: + bundle.install_mime_type(bundle_path) + except MalformedBundleException: + logging.exception('Error loading bundle %r', bundle_path) + return None + + bundle_id = bundle.get_bundle_id() + installed = self.get_bundle(bundle_id) + + if installed is not None: + if NormalizedVersion(installed.get_activity_version()) >= \ + NormalizedVersion(bundle.get_activity_version()): + logging.debug('Skip old version for %s', bundle_id) + return None + else: + logging.debug('Upgrade %s', bundle_id) + self.remove_bundle(installed.get_path()) + + self._bundles.append(bundle) + return bundle + + def remove_bundle(self, bundle_path): + for bundle in self._bundles: + if bundle.get_path() == bundle_path: + self._bundles.remove(bundle) + self.emit('bundle-removed', bundle) + return True + return False + + def get_activities_for_type(self, mime_type): + result = [] + + mime = mimeregistry.get_registry() + default_bundle_id = mime.get_default_activity(mime_type) + default_bundle = None + + for bundle in self._bundles: + if mime_type in (bundle.get_mime_types() or []): + if bundle.get_bundle_id() == default_bundle_id: + default_bundle = bundle + elif self.get_default_for_type(mime_type) == \ + bundle.get_bundle_id(): + result.insert(0, bundle) + else: + result.append(bundle) + + if default_bundle is not None: + result.insert(0, default_bundle) + + return result + + def get_default_for_type(self, mime_type): + return self._mime_defaults.get(mime_type) + + def _find_bundle(self, bundle_id, version): + for bundle in self._bundles: + if bundle.get_bundle_id() == bundle_id and \ + bundle.get_activity_version() == version: + return bundle + raise ValueError('No bundle %r with version %r exists.' % \ + (bundle_id, version)) + + def set_bundle_favorite(self, bundle_id, version, favorite): + changed = self._set_bundle_favorite(bundle_id, version, favorite) + if changed: + bundle = self._find_bundle(bundle_id, version) + self.emit('bundle-changed', bundle) + + def _set_bundle_favorite(self, bundle_id, version, favorite): + key = self._get_favorite_key(bundle_id, version) + if favorite and not key in self._favorite_bundles: + self._favorite_bundles[key] = None + elif not favorite and key in self._favorite_bundles: + del self._favorite_bundles[key] + else: + return False + + self._write_favorites_file() + return True + + def is_bundle_favorite(self, bundle_id, version): + key = self._get_favorite_key(bundle_id, version) + return key in self._favorite_bundles + + def is_activity_protected(self, bundle_id): + return bundle_id in self._protected_activities + + def set_bundle_position(self, bundle_id, version, x, y): + key = self._get_favorite_key(bundle_id, version) + if key not in self._favorite_bundles: + raise ValueError('Bundle %s %s not favorite' % + (bundle_id, version)) + + if self._favorite_bundles[key] is None: + self._favorite_bundles[key] = {} + if 'position' not in self._favorite_bundles[key] or \ + [x, y] != self._favorite_bundles[key]['position']: + self._favorite_bundles[key]['position'] = [x, y] + else: + return + + self._write_favorites_file() + bundle = self._find_bundle(bundle_id, version) + self.emit('bundle-changed', bundle) + + def get_bundle_position(self, bundle_id, version): + """Get the coordinates where the user wants the representation of this + bundle to be displayed. Coordinates are relative to a 1000x1000 area. + """ + key = self._get_favorite_key(bundle_id, version) + if key not in self._favorite_bundles or \ + self._favorite_bundles[key] is None or \ + 'position' not in self._favorite_bundles[key]: + return (-1, -1) + else: + return tuple(self._favorite_bundles[key]['position']) + + def _write_favorites_file(self): + path = env.get_profile_path('favorite_activities') + favorites_data = {'defaults-mtime': self._last_defaults_mtime, + 'favorites': self._favorite_bundles} + simplejson.dump(favorites_data, open(path, 'w'), indent=1) + + def is_installed(self, bundle): + # TODO treat ContentBundle in special way + # needs rethinking while fixing ContentBundle support + if isinstance(bundle, ContentBundle) or \ + isinstance(bundle, JournalEntryBundle): + return bundle.is_installed() + + for installed_bundle in self._bundles: + if bundle.get_bundle_id() == installed_bundle.get_bundle_id() and \ + NormalizedVersion(bundle.get_activity_version()) == \ + NormalizedVersion(installed_bundle.get_activity_version()): + return True + return False + + def install(self, bundle, uid=None, force_downgrade=False): + activities_path = env.get_user_activities_path() + + for installed_bundle in self._bundles: + if bundle.get_bundle_id() == installed_bundle.get_bundle_id() and \ + NormalizedVersion(bundle.get_activity_version()) <= \ + NormalizedVersion(installed_bundle.get_activity_version()): + if not force_downgrade: + raise AlreadyInstalledException + else: + self.uninstall(installed_bundle, force=True) + elif bundle.get_bundle_id() == installed_bundle.get_bundle_id(): + self.uninstall(installed_bundle, force=True) + + install_dir = env.get_user_activities_path() + if isinstance(bundle, JournalEntryBundle): + install_path = bundle.install(uid) + elif isinstance(bundle, ContentBundle): + install_path = bundle.install() + else: + install_path = bundle.install(install_dir) + + # TODO treat ContentBundle in special way + # needs rethinking while fixing ContentBundle support + if isinstance(bundle, ContentBundle) or \ + isinstance(bundle, JournalEntryBundle): + pass + elif not self.add_bundle(install_path): + raise RegistrationException + + def uninstall(self, bundle, force=False, delete_profile=False): + # TODO treat ContentBundle in special way + # needs rethinking while fixing ContentBundle support + if isinstance(bundle, ContentBundle) or \ + isinstance(bundle, JournalEntryBundle): + if bundle.is_installed(): + bundle.uninstall() + else: + logging.warning('Not uninstalling, bundle is not installed') + return + + act = self.get_bundle(bundle.get_bundle_id()) + if not force and \ + act.get_activity_version() != bundle.get_activity_version(): + logging.warning('Not uninstalling, different bundle present') + return + + if not act.is_user_activity(): + logging.debug('Do not uninstall system activity') + return + + install_path = act.get_path() + + bundle.uninstall(install_path, force, delete_profile) + + if not self.remove_bundle(install_path): + raise RegistrationException + + def upgrade(self, bundle): + act = self.get_bundle(bundle.get_bundle_id()) + if act is None: + logging.warning('Activity not installed') + elif act.get_activity_version() == bundle.get_activity_version(): + logging.debug('No upgrade needed, same version already installed.') + return + elif act.is_user_activity(): + try: + self.uninstall(bundle, force=True) + except Exception: + logging.exception('Uninstall failed, still trying to install' + ' newer bundle:') + else: + logging.warning('Unable to uninstall system activity, ' + 'installing upgraded version in user activities') + + self.install(bundle) + + +def get_registry(): + global _instance + if not _instance: + _instance = BundleRegistry() + return _instance diff --git a/src/jarabe/model/filetransfer.py b/src/jarabe/model/filetransfer.py new file mode 100644 index 0000000..710c3a4 --- /dev/null +++ b/src/jarabe/model/filetransfer.py @@ -0,0 +1,368 @@ +# Copyright (C) 2008 Tomeu Vizoso +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +import socket + +import gobject +import gio +import dbus +from telepathy.interfaces import CONNECTION_INTERFACE_REQUESTS, CHANNEL +from telepathy.constants import CONNECTION_HANDLE_TYPE_CONTACT, \ + SOCKET_ADDRESS_TYPE_UNIX, \ + SOCKET_ACCESS_CONTROL_LOCALHOST +from telepathy.client import Connection, Channel + +from sugar.presence import presenceservice +from sugar import dispatch + +from jarabe.util.telepathy import connection_watcher +from jarabe.model import neighborhood + + +FT_STATE_NONE = 0 +FT_STATE_PENDING = 1 +FT_STATE_ACCEPTED = 2 +FT_STATE_OPEN = 3 +FT_STATE_COMPLETED = 4 +FT_STATE_CANCELLED = 5 + +FT_REASON_NONE = 0 +FT_REASON_REQUESTED = 1 +FT_REASON_LOCAL_STOPPED = 2 +FT_REASON_REMOTE_STOPPED = 3 +FT_REASON_LOCAL_ERROR = 4 +FT_REASON_LOCAL_ERROR = 5 +FT_REASON_REMOTE_ERROR = 6 + +# FIXME: use constants from tp-python once the spec is undrafted +CHANNEL_TYPE_FILE_TRANSFER = \ + 'org.freedesktop.Telepathy.Channel.Type.FileTransfer' + +new_file_transfer = dispatch.Signal() + + +# TODO Move to use splice_async() in Sugar 0.88 +class StreamSplicer(gobject.GObject): + _CHUNK_SIZE = 10240 # 10K + __gsignals__ = { + 'finished': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([])), + } + + def __init__(self, input_stream, output_stream): + gobject.GObject.__init__(self) + + self._input_stream = input_stream + self._output_stream = output_stream + self._pending_buffers = [] + + def start(self): + self._input_stream.read_async(self._CHUNK_SIZE, self.__read_async_cb, + gobject.PRIORITY_LOW) + + def __read_async_cb(self, input_stream, result): + data = input_stream.read_finish(result) + + if not data: + logging.debug('closing input stream') + self._input_stream.close() + else: + self._pending_buffers.append(data) + self._input_stream.read_async(self._CHUNK_SIZE, + self.__read_async_cb, + gobject.PRIORITY_LOW) + self._write_next_buffer() + + def __write_async_cb(self, output_stream, result, user_data): + count_ = output_stream.write_finish(result) + + if not self._pending_buffers and \ + not self._output_stream.has_pending() and \ + not self._input_stream.has_pending(): + logging.debug('closing output stream') + output_stream.close() + self.emit('finished') + else: + self._write_next_buffer() + + def _write_next_buffer(self): + if self._pending_buffers and not self._output_stream.has_pending(): + data = self._pending_buffers.pop(0) + # TODO: we pass the buffer as user_data because of + # http://bugzilla.gnome.org/show_bug.cgi?id=564102 + self._output_stream.write_async(data, self.__write_async_cb, + gobject.PRIORITY_LOW, + user_data=data) + + +class BaseFileTransfer(gobject.GObject): + + def __init__(self, connection): + gobject.GObject.__init__(self) + self._connection = connection + self._state = FT_STATE_NONE + self._transferred_bytes = 0 + + self.channel = None + self.buddy = None + self.title = None + self.file_size = None + self.description = None + self.mime_type = None + self.initial_offset = 0 + self.reason_last_change = FT_REASON_NONE + + def set_channel(self, channel): + self.channel = channel + self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + 'FileTransferStateChanged', self.__state_changed_cb) + self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + 'TransferredBytesChanged', self.__transferred_bytes_changed_cb) + self.channel[CHANNEL_TYPE_FILE_TRANSFER].connect_to_signal( + 'InitialOffsetDefined', self.__initial_offset_defined_cb) + + channel_properties = self.channel[dbus.PROPERTIES_IFACE] + + props = channel_properties.GetAll(CHANNEL_TYPE_FILE_TRANSFER) + self._state = props['State'] + self.title = props['Filename'] + self.file_size = props['Size'] + self.description = props['Description'] + self.mime_type = props['ContentType'] + + handle = channel_properties.Get(CHANNEL, 'TargetHandle') + self.buddy = neighborhood.get_model().get_buddy_by_handle(handle) + + def __transferred_bytes_changed_cb(self, transferred_bytes): + logging.debug('__transferred_bytes_changed_cb %r', transferred_bytes) + self.props.transferred_bytes = transferred_bytes + + def _set_transferred_bytes(self, transferred_bytes): + self._transferred_bytes = transferred_bytes + + def _get_transferred_bytes(self): + return self._transferred_bytes + + transferred_bytes = gobject.property(type=int, default=0, + getter=_get_transferred_bytes, setter=_set_transferred_bytes) + + def __initial_offset_defined_cb(self, offset): + logging.debug('__initial_offset_defined_cb %r', offset) + self.initial_offset = offset + + def __state_changed_cb(self, state, reason): + logging.debug('__state_changed_cb %r %r', state, reason) + self.reason_last_change = reason + self.props.state = state + + def _set_state(self, state): + self._state = state + + def _get_state(self): + return self._state + + state = gobject.property(type=int, getter=_get_state, setter=_set_state) + + def cancel(self): + self.channel[CHANNEL].Close() + + +class IncomingFileTransfer(BaseFileTransfer): + def __init__(self, connection, object_path, props): + BaseFileTransfer.__init__(self, connection) + + channel = Channel(connection.service_name, object_path) + self.set_channel(channel) + + self.connect('notify::state', self.__notify_state_cb) + + self.destination_path = None + self._socket_address = None + self._socket = None + self._splicer = None + + def accept(self, destination_path): + if os.path.exists(destination_path): + raise ValueError('Destination path already exists: %r' % \ + destination_path) + + self.destination_path = destination_path + + channel_ft = self.channel[CHANNEL_TYPE_FILE_TRANSFER] + self._socket_address = channel_ft.AcceptFile(SOCKET_ADDRESS_TYPE_UNIX, + SOCKET_ACCESS_CONTROL_LOCALHOST, '', 0, byte_arrays=True) + + def __notify_state_cb(self, file_transfer, pspec): + logging.debug('__notify_state_cb %r', self.props.state) + if self.props.state == FT_STATE_OPEN: + # Need to hold a reference to the socket so that python doesn't + # close the fd when it goes out of scope + self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.connect(self._socket_address) + input_stream = gio.unix.InputStream(self._socket.fileno(), True) + + destination_file = gio.File(self.destination_path) + if self.initial_offset == 0: + output_stream = destination_file.create() + else: + output_stream = destination_file.append_to() + + # TODO: Use splice_async when it gets implemented + self._splicer = StreamSplicer(input_stream, output_stream) + self._splicer.start() + + +class OutgoingFileTransfer(BaseFileTransfer): + def __init__(self, buddy, file_name, title, description, mime_type): + + presence_service = presenceservice.get_instance() + name, path = presence_service.get_preferred_connection() + connection = Connection(name, path, + ready_handler=self.__connection_ready_cb) + + BaseFileTransfer.__init__(self, connection) + self.connect('notify::state', self.__notify_state_cb) + + self._file_name = file_name + self._socket_address = None + self._socket = None + self._splicer = None + self._output_stream = None + + self.buddy = buddy + self.title = title + self.file_size = os.stat(file_name).st_size + self.description = description + self.mime_type = mime_type + + def __connection_ready_cb(self, connection): + requests = connection[CONNECTION_INTERFACE_REQUESTS] + object_path, properties_ = requests.CreateChannel({ + CHANNEL + '.ChannelType': CHANNEL_TYPE_FILE_TRANSFER, + CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_CONTACT, + CHANNEL + '.TargetHandle': self.buddy.handle, + CHANNEL_TYPE_FILE_TRANSFER + '.ContentType': self.mime_type, + CHANNEL_TYPE_FILE_TRANSFER + '.Filename': self.title, + CHANNEL_TYPE_FILE_TRANSFER + '.Size': self.file_size, + CHANNEL_TYPE_FILE_TRANSFER + '.Description': self.description, + CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset': 0}) + + self.set_channel(Channel(connection.service_name, object_path)) + + channel_file_transfer = self.channel[CHANNEL_TYPE_FILE_TRANSFER] + self._socket_address = channel_file_transfer.ProvideFile( + SOCKET_ADDRESS_TYPE_UNIX, SOCKET_ACCESS_CONTROL_LOCALHOST, '', + byte_arrays=True) + + def __notify_state_cb(self, file_transfer, pspec): + logging.debug('__notify_state_cb %r', self.props.state) + if self.props.state == FT_STATE_OPEN: + # Need to hold a reference to the socket so that python doesn't + # closes the fd when it goes out of scope + self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.connect(self._socket_address) + output_stream = gio.unix.OutputStream(self._socket.fileno(), True) + + logging.debug('opening %s for reading', self._file_name) + input_stream = gio.File(self._file_name).read() + if self.initial_offset > 0: + input_stream.skip(self.initial_offset) + + # TODO: Use splice_async when it gets implemented + self._splicer = StreamSplicer(input_stream, output_stream) + self._splicer.start() + + def cancel(self): + self.channel[CHANNEL].Close() + + +def _new_channels_cb(connection, channels): + for object_path, props in channels: + if props[CHANNEL + '.ChannelType'] == CHANNEL_TYPE_FILE_TRANSFER and \ + not props[CHANNEL + '.Requested']: + + logging.debug('__new_channels_cb %r', object_path) + + incoming_file_transfer = IncomingFileTransfer(connection, + object_path, props) + new_file_transfer.send(None, file_transfer=incoming_file_transfer) + + +def _monitor_connection(connection): + logging.debug('connection added %r', connection) + connection[CONNECTION_INTERFACE_REQUESTS].connect_to_signal('NewChannels', + lambda channels: _new_channels_cb(connection, channels)) + + +def _connection_added_cb(conn_watcher, connection): + _monitor_connection(connection) + + +def _connection_removed_cb(conn_watcher, connection): + logging.debug('connection removed %r', connection) + + +def init(): + conn_watcher = connection_watcher.get_instance() + conn_watcher.connect('connection-added', _connection_added_cb) + conn_watcher.connect('connection-removed', _connection_removed_cb) + + for connection in conn_watcher.get_connections(): + _monitor_connection(connection) + + +def start_transfer(buddy, file_name, title, description, mime_type): + outgoing_file_transfer = OutgoingFileTransfer(buddy, file_name, title, + description, mime_type) + new_file_transfer.send(None, file_transfer=outgoing_file_transfer) + + +def file_transfer_available(): + conn_watcher = connection_watcher.get_instance() + for connection in conn_watcher.get_connections(): + + properties_iface = connection[dbus.PROPERTIES_IFACE] + properties = properties_iface.GetAll(CONNECTION_INTERFACE_REQUESTS) + classes = properties['RequestableChannelClasses'] + for prop, allowed_prop in classes: + + channel_type = prop.get(CHANNEL + '.ChannelType', '') + target_handle_type = prop.get(CHANNEL + '.TargetHandleType', '') + + if len(prop) == 2 and \ + channel_type == CHANNEL_TYPE_FILE_TRANSFER and \ + target_handle_type == CONNECTION_HANDLE_TYPE_CONTACT: + return True + + return False + + +if __name__ == '__main__': + import tempfile + + test_file_name = '/home/tomeu/isos/Soas2-200904031934.iso' + test_input_stream = gio.File(test_file_name).read() + test_output_stream = gio.File(tempfile.mkstemp()[1]).append_to() + + # TODO: Use splice_async when it gets implemented + splicer = StreamSplicer(test_input_stream, test_output_stream) + splicer.start() + + loop = gobject.MainLoop() + loop.run() diff --git a/src/jarabe/model/friends.py b/src/jarabe/model/friends.py new file mode 100644 index 0000000..7605af1 --- /dev/null +++ b/src/jarabe/model/friends.py @@ -0,0 +1,174 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +from ConfigParser import ConfigParser + +import gobject +import dbus + +from sugar import env +from sugar.graphics.xocolor import XoColor + +from jarabe.model.buddy import BuddyModel +from jarabe.model import neighborhood + + +_model = None + + +class FriendBuddyModel(BuddyModel): + __gtype_name__ = 'SugarFriendBuddyModel' + + _NOT_PRESENT_COLOR = '#D5D5D5,#FFFFFF' + + def __init__(self, nick, key, account=None, contact_id=None): + self._online_buddy = None + + BuddyModel.__init__(self, nick=nick, key=key, account=account, + contact_id=contact_id) + + neighborhood_model = neighborhood.get_model() + neighborhood_model.connect('buddy-added', self.__buddy_added_cb) + neighborhood_model.connect('buddy-removed', self.__buddy_removed_cb) + + buddy = neighborhood_model.get_buddy_by_key(key) + if buddy is not None: + self._set_online_buddy(buddy) + + def __buddy_added_cb(self, model_, buddy): + if buddy.key != self.key: + return + self._set_online_buddy(buddy) + + def _set_online_buddy(self, buddy): + self._online_buddy = buddy + self._online_buddy.connect('notify::color', self.__notify_color_cb) + self.notify('color') + self.notify('present') + + if buddy.nick != self.nick: + self.nick = buddy.nick + if buddy.contact_id != self.contact_id: + self.contact_id = buddy.contact_id + if buddy.account != self.account: + self.account = buddy.account + + def __buddy_removed_cb(self, model_, buddy): + if buddy.key != self.key: + return + self._online_buddy = None + self.notify('color') + self.notify('present') + + def __notify_color_cb(self, buddy, pspec): + self.notify('color') + + def is_present(self): + return self._online_buddy is not None + + present = gobject.property(type=bool, default=False, getter=is_present) + + def get_color(self): + if self._online_buddy is not None: + return self._online_buddy.color + else: + return XoColor(FriendBuddyModel._NOT_PRESENT_COLOR) + + color = gobject.property(type=object, getter=get_color) + + def get_handle(self): + if self._online_buddy is not None: + return self._online_buddy.handle + else: + return None + + handle = gobject.property(type=object, getter=get_handle) + + +class Friends(gobject.GObject): + __gsignals__ = { + 'friend-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'friend-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([str])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._friends = {} + self._path = os.path.join(env.get_profile_path(), 'friends') + + self.load() + + def has_buddy(self, buddy): + return buddy.get_key() in self._friends + + def add_friend(self, buddy_info): + self._friends[buddy_info.get_key()] = buddy_info + self.emit('friend-added', buddy_info) + + def make_friend(self, buddy): + if not self.has_buddy(buddy): + buddy = FriendBuddyModel(key=buddy.key, nick=buddy.nick, + account=buddy.account, + contact_id=buddy.contact_id) + self.add_friend(buddy) + self.save() + + def remove(self, buddy_info): + del self._friends[buddy_info.get_key()] + self.save() + self.emit('friend-removed', buddy_info.get_key()) + + def __iter__(self): + return self._friends.values().__iter__() + + def load(self): + cp = ConfigParser() + + try: + success = cp.read([self._path]) + if success: + for key in cp.sections(): + # HACK: don't screw up on old friends files + if len(key) < 20: + continue + buddy = FriendBuddyModel(key=key, nick=cp.get(key, 'nick')) + self.add_friend(buddy) + except Exception: + logging.exception('Error parsing friends file') + + def save(self): + cp = ConfigParser() + + for friend in self: + section = friend.get_key() + cp.add_section(section) + cp.set(section, 'nick', friend.get_nick()) + + fileobject = open(self._path, 'w') + cp.write(fileobject) + fileobject.close() + + +def get_model(): + global _model + if _model is None: + _model = Friends() + return _model diff --git a/src/jarabe/model/invites.py b/src/jarabe/model/invites.py new file mode 100644 index 0000000..631e49f --- /dev/null +++ b/src/jarabe/model/invites.py @@ -0,0 +1,289 @@ +# Copyright (C) 2006-2007 Red Hat, Inc. +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from functools import partial +import simplejson + +import gobject +import dbus +import gconf +from telepathy.interfaces import CHANNEL, \ + CHANNEL_DISPATCHER, \ + CHANNEL_DISPATCH_OPERATION, \ + CHANNEL_TYPE_CONTACT_LIST, \ + CHANNEL_TYPE_TEXT, \ + CLIENT +from telepathy.constants import HANDLE_TYPE_ROOM + +from sugar.graphics.xocolor import XoColor + +from jarabe.model import telepathyclient +from jarabe.model import bundleregistry +from jarabe.model import neighborhood +from jarabe.journal import misc + + +CONNECTION_INTERFACE_ACTIVITY_PROPERTIES = \ + 'org.laptop.Telepathy.ActivityProperties' + +_instance = None + + +class BaseInvite(object): + """Invitation to shared activity or private 1-1 Telepathy channel""" + def __init__(self, dispatch_operation_path, handle, handler): + self.dispatch_operation_path = dispatch_operation_path + self._handle = handle + self._handler = handler + + def get_bundle_id(self): + if CLIENT in self._handler: + return self._handler[len(CLIENT + '.'):] + else: + return None + + def _call_handle_with(self): + bus = dbus.Bus() + obj = bus.get_object(CHANNEL_DISPATCHER, self.dispatch_operation_path) + dispatch_operation = dbus.Interface(obj, CHANNEL_DISPATCH_OPERATION) + dispatch_operation.HandleWith(self._handler, + reply_handler=self._handle_with_reply_cb, + error_handler=self._handle_with_reply_cb) + + def _handle_with_reply_cb(self, error=None): + if error is not None: + raise error + else: + logging.debug('_handle_with_reply_cb') + + def _name_owner_changed_cb(self, name, old_owner, new_owner): + logging.debug('BaseInvite._name_owner_changed_cb %r %r %r', name, + new_owner, old_owner) + if name == self._handler and new_owner and not old_owner: + self._call_handle_with() + + +class ActivityInvite(BaseInvite): + """Invitation to a shared activity.""" + def __init__(self, dispatch_operation_path, handle, handler, + activity_properties): + BaseInvite.__init__(self, dispatch_operation_path, handle, handler) + + if activity_properties is not None: + self._activity_properties = activity_properties + else: + self._activity_properties = {} + + def get_color(self): + color = self._activity_properties.get('color', None) + return XoColor(color) + + def join(self): + logging.error('ActivityInvite.join handler %r', self._handler) + + registry = bundleregistry.get_registry() + bundle_id = self.get_bundle_id() + bundle = registry.get_bundle(bundle_id) + if bundle is None: + self._call_handle_with() + return + + bus = dbus.SessionBus() + bus.add_signal_receiver(self._name_owner_changed_cb, + 'NameOwnerChanged', + 'org.freedesktop.DBus', + arg0=self._handler) + + model = neighborhood.get_model() + activity_id = model.get_activity_by_room(self._handle).activity_id + misc.launch(bundle, color=self.get_color(), invited=True, + activity_id=activity_id) + + +class PrivateInvite(BaseInvite): + def __init__(self, dispatch_operation_path, handle, handler, + private_channel): + BaseInvite.__init__(self, dispatch_operation_path, handle, handler) + + self._private_channel = private_channel + + def get_color(self): + client = gconf.client_get_default() + return XoColor(client.get_string('/desktop/sugar/user/color')) + + def join(self): + logging.error('PrivateInvite.join handler %r', self._handler) + registry = bundleregistry.get_registry() + bundle_id = self.get_bundle_id() + bundle = registry.get_bundle(bundle_id) + + bus = dbus.SessionBus() + bus.add_signal_receiver(self._name_owner_changed_cb, + 'NameOwnerChanged', + 'org.freedesktop.DBus', + arg0=self._handler) + misc.launch(bundle, color=self.get_color(), invited=True, + uri=self._private_channel) + + +class Invites(gobject.GObject): + __gsignals__ = { + 'invite-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'invite-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._dispatch_operations = {} + + client_handler = telepathyclient.get_instance() + client_handler.got_dispatch_operation.connect( + self.__got_dispatch_operation_cb) + + def __got_dispatch_operation_cb(self, **kwargs): + logging.debug('__got_dispatch_operation_cb') + dispatch_operation_path = kwargs['dispatch_operation_path'] + channel_path, channel_properties = kwargs['channels'][0] + properties = kwargs['properties'] + channel_type = channel_properties[CHANNEL + '.ChannelType'] + handle_type = channel_properties[CHANNEL + '.TargetHandleType'] + handle = channel_properties[CHANNEL + '.TargetHandle'] + + if handle_type == HANDLE_TYPE_ROOM and \ + channel_type == CHANNEL_TYPE_TEXT: + logging.debug('May be an activity, checking its properties') + connection_path = properties[CHANNEL_DISPATCH_OPERATION + + '.Connection'] + connection_name = connection_path.replace('/', '.')[1:] + + bus = dbus.Bus() + connection = bus.get_object(connection_name, connection_path) + connection.GetProperties( + channel_properties[CHANNEL + '.TargetHandle'], + dbus_interface=CONNECTION_INTERFACE_ACTIVITY_PROPERTIES, + reply_handler=partial(self.__get_properties_cb, + handle, + dispatch_operation_path), + error_handler=partial(self.__error_handler_cb, + handle, + channel_properties, + dispatch_operation_path, + channel_path, + properties)) + else: + self._dispatch_non_sugar_invitation(handle, + channel_properties, + dispatch_operation_path, + channel_path, + properties) + + def __get_properties_cb(self, handle, dispatch_operation_path, properties): + logging.debug('__get_properties_cb %r', properties) + handler = '%s.%s' % (CLIENT, properties['type']) + self._add_invite(dispatch_operation_path, handle, handler, properties) + + def __error_handler_cb(self, handle, channel_properties, + dispatch_operation_path, channel_path, + properties, error): + logging.debug('__error_handler_cb %r', error) + exception_name = 'org.freedesktop.Telepathy.Error.NotAvailable' + if error.get_dbus_name() == exception_name: + self._dispatch_non_sugar_invitation(handle, + channel_properties, + dispatch_operation_path, + channel_path, + properties) + else: + raise error + + def _dispatch_non_sugar_invitation(self, handle, channel_properties, + dispatch_operation_path, channel_path, + properties): + handler = None + channel_type = channel_properties[CHANNEL + '.ChannelType'] + if channel_type == CHANNEL_TYPE_CONTACT_LIST: + self._handle_with(dispatch_operation_path, CLIENT + '.Sugar') + elif channel_type == CHANNEL_TYPE_TEXT: + handler = CLIENT + '.org.laptop.Chat' + self._add_private_invite(dispatch_operation_path, handle, handler, + channel_path, properties) + return + else: + self._call_handle_with(dispatch_operation_path, '') + + if handler is not None: + logging.debug('Adding an invite from a non-Sugar client') + self._add_invite(dispatch_operation_path, handle, handler) + + def _call_handle_with(self, dispatch_operation_path, handler): + logging.debug('_handle_with %r %r', dispatch_operation_path, handler) + bus = dbus.Bus() + obj = bus.get_object(CHANNEL_DISPATCHER, dispatch_operation_path) + dispatch_operation = dbus.Interface(obj, CHANNEL_DISPATCH_OPERATION) + dispatch_operation.HandleWith(handler, + reply_handler=self.__handle_with_reply_cb, + error_handler=self.__handle_with_reply_cb) + + def __handle_with_reply_cb(self, error=None): + if error is not None: + logging.error('__handle_with_reply_cb %r', error) + else: + logging.debug('__handle_with_reply_cb') + + def _add_invite(self, dispatch_operation_path, handle, handler, + activity_properties=None): + logging.debug('_add_invite %r %r %r', dispatch_operation_path, handle, + handler) + if dispatch_operation_path in self._dispatch_operations: + # there is no point to have more than one invite for the same + # dispatch operation + return + + invite = ActivityInvite(dispatch_operation_path, handle, handler, + activity_properties) + self._dispatch_operations[dispatch_operation_path] = invite + self.emit('invite-added', invite) + + def _add_private_invite(self, dispatch_operation_path, handle, handler, + channel_path, properties): + connection_path = properties[CHANNEL_DISPATCH_OPERATION + + '.Connection'] + connection_name = connection_path.replace('/', '.')[1:] + private_channel = simplejson.dumps([connection_name, + connection_path, channel_path]) + invite = PrivateInvite(dispatch_operation_path, handle, handler, + private_channel) + self._dispatch_operations[dispatch_operation_path] = invite + self.emit('invite-added', invite) + + def remove_invite(self, invite): + del self._dispatch_operations[invite.dispatch_operation_path] + self.emit('invite-removed', invite) + + def __iter__(self): + return self._dispatch_operations.values().__iter__() + + +def get_instance(): + global _instance + if not _instance: + _instance = Invites() + return _instance diff --git a/src/jarabe/model/mimeregistry.py b/src/jarabe/model/mimeregistry.py new file mode 100644 index 0000000..7fb5bcf --- /dev/null +++ b/src/jarabe/model/mimeregistry.py @@ -0,0 +1,50 @@ +# Copyright (C) 2009 Aleksey Lim +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import re + +import gconf + + +_DEFAULTS_KEY = '/desktop/sugar/journal/defaults' +_GCONF_INVALID_CHARS = re.compile('[^a-zA-Z0-9-_/.]') + +_instance = None + + +class MimeRegistry(object): + + def __init__(self): + # TODO move here all mime_type related code from jarabe modules + self._gconf = gconf.client_get_default() + + def get_default_activity(self, mime_type): + return self._gconf.get_string(_key_name(mime_type)) + + def set_default_activity(self, mime_type, bundle_id): + self._gconf.set_string(_key_name(mime_type), bundle_id) + + +def get_registry(): + global _instance + if _instance is None: + _instance = MimeRegistry() + return _instance + + +def _key_name(mime_type): + mime_type = _GCONF_INVALID_CHARS.sub('_', mime_type) + return '%s/%s' % (_DEFAULTS_KEY, mime_type) diff --git a/src/jarabe/model/neighborhood.py b/src/jarabe/model/neighborhood.py new file mode 100644 index 0000000..828cb14 --- /dev/null +++ b/src/jarabe/model/neighborhood.py @@ -0,0 +1,1084 @@ +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +from functools import partial +from hashlib import sha1 + +import gobject +import gconf +import dbus +from dbus import PROPERTIES_IFACE +from telepathy.interfaces import ACCOUNT, \ + ACCOUNT_MANAGER, \ + CHANNEL, \ + CHANNEL_INTERFACE_GROUP, \ + CHANNEL_TYPE_CONTACT_LIST, \ + CHANNEL_TYPE_FILE_TRANSFER, \ + CLIENT, \ + CONNECTION, \ + CONNECTION_INTERFACE_ALIASING, \ + CONNECTION_INTERFACE_CONTACTS, \ + CONNECTION_INTERFACE_CONTACT_CAPABILITIES, \ + CONNECTION_INTERFACE_REQUESTS, \ + CONNECTION_INTERFACE_SIMPLE_PRESENCE +from telepathy.constants import HANDLE_TYPE_CONTACT, \ + HANDLE_TYPE_LIST, \ + CONNECTION_PRESENCE_TYPE_OFFLINE, \ + CONNECTION_STATUS_CONNECTED, \ + CONNECTION_STATUS_DISCONNECTED +from telepathy.client import Connection, Channel + +from sugar.graphics.xocolor import XoColor +from sugar.profile import get_profile + +from jarabe.model.buddy import BuddyModel, get_owner_instance +from jarabe.model import bundleregistry +from jarabe.model import shell + + +ACCOUNT_MANAGER_SERVICE = 'org.freedesktop.Telepathy.AccountManager' +ACCOUNT_MANAGER_PATH = '/org/freedesktop/Telepathy/AccountManager' +CHANNEL_DISPATCHER_SERVICE = 'org.freedesktop.Telepathy.ChannelDispatcher' +CHANNEL_DISPATCHER_PATH = '/org/freedesktop/Telepathy/ChannelDispatcher' +SUGAR_CLIENT_SERVICE = 'org.freedesktop.Telepathy.Client.Sugar' +SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar' + +CONNECTION_INTERFACE_BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' +CONNECTION_INTERFACE_ACTIVITY_PROPERTIES = \ + 'org.laptop.Telepathy.ActivityProperties' + +_QUERY_DBUS_TIMEOUT = 200 +""" +Time in seconds to wait when querying contact properties. Some jabber servers +will be very slow in returning these queries, so just be patient. +""" + +_model = None + + +class ActivityModel(gobject.GObject): + __gsignals__ = { + 'current-buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'current-buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self, activity_id, room_handle): + gobject.GObject.__init__(self) + + self.activity_id = activity_id + self.room_handle = room_handle + self._bundle = None + self._color = None + self._private = True + self._name = None + self._current_buddies = [] + self._buddies = [] + + def get_color(self): + return self._color + + def set_color(self, color): + self._color = color + + color = gobject.property(type=object, getter=get_color, setter=set_color) + + def get_bundle(self): + return self._bundle + + def set_bundle(self, bundle): + self._bundle = bundle + + bundle = gobject.property(type=object, getter=get_bundle, + setter=set_bundle) + + def get_name(self): + return self._name + + def set_name(self, name): + self._name = name + + name = gobject.property(type=object, getter=get_name, setter=set_name) + + def is_private(self): + return self._private + + def set_private(self, private): + self._private = private + + private = gobject.property(type=object, getter=is_private, + setter=set_private) + + def get_buddies(self): + return self._buddies + + def add_buddy(self, buddy): + self._buddies.append(buddy) + self.notify('buddies') + self.emit('buddy-added', buddy) + + def remove_buddy(self, buddy): + self._buddies.remove(buddy) + self.notify('buddies') + self.emit('buddy-removed', buddy) + + buddies = gobject.property(type=object, getter=get_buddies) + + def get_current_buddies(self): + return self._current_buddies + + def add_current_buddy(self, buddy): + self._current_buddies.append(buddy) + self.notify('current-buddies') + self.emit('current-buddy-added', buddy) + + def remove_current_buddy(self, buddy): + self._current_buddies.remove(buddy) + self.notify('current-buddies') + self.emit('current-buddy-removed', buddy) + + current_buddies = gobject.property(type=object, getter=get_current_buddies) + + +class _Account(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'activity-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object, object])), + 'buddy-updated': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-joined-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'buddy-left-activity': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object, object])), + 'current-activity-updated': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ([object, object])), + 'connected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + 'disconnected': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ([])), + } + + def __init__(self, account_path): + gobject.GObject.__init__(self) + + self.object_path = account_path + + self._connection = None + self._buddy_handles = {} + self._activity_handles = {} + self._self_handle = None + + self._buddies_per_activity = {} + self._activities_per_buddy = {} + + self._start_listening() + + def _start_listening(self): + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Get(ACCOUNT, 'Connection', + reply_handler=self.__got_connection_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.GetConnection')) + obj.connect_to_signal( + 'AccountPropertyChanged', self.__account_property_changed_cb) + + def __error_handler_cb(self, function_name, error): + raise RuntimeError('Error when calling %s: %s' % (function_name, + error)) + + def __got_connection_cb(self, connection_path): + logging.debug('_Account.__got_connection_cb %r', connection_path) + + if connection_path == '/': + self._check_registration_error() + return + + self._prepare_connection(connection_path) + + def _check_registration_error(self): + """ + See if a previous connection attempt failed and we need to unset + the register flag. + """ + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Get(ACCOUNT, 'ConnectionError', + reply_handler=self.__got_connection_error_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.GetConnectionError')) + + def __got_connection_error_cb(self, error): + logging.debug('_Account.__got_connection_error_cb %r', error) + if error == 'org.freedesktop.Telepathy.Error.RegistrationExists': + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.UpdateParameters({'register': False}, [], + dbus_interface=ACCOUNT) + + def __account_property_changed_cb(self, properties): + logging.debug('_Account.__account_property_changed_cb %r %r %r', + self.object_path, properties.get('Connection', None), + self._connection) + if 'Connection' not in properties: + return + if properties['Connection'] == '/': + self._check_registration_error() + self._connection = None + elif self._connection is None: + self._prepare_connection(properties['Connection']) + + def _prepare_connection(self, connection_path): + connection_name = connection_path.replace('/', '.')[1:] + + self._connection = Connection(connection_name, connection_path, + ready_handler=self.__connection_ready_cb) + + def __connection_ready_cb(self, connection): + logging.debug('_Account.__connection_ready_cb %r', + connection.object_path) + connection.connect_to_signal('StatusChanged', + self.__status_changed_cb) + + connection[PROPERTIES_IFACE].Get(CONNECTION, + 'Status', + reply_handler=self.__get_status_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetStatus')) + + def __get_status_cb(self, status): + logging.debug('_Account.__get_status_cb %r %r', + self._connection.object_path, status) + self._update_status(status) + + def __status_changed_cb(self, status, reason): + logging.debug('_Account.__status_changed_cb %r %r', status, reason) + self._update_status(status) + + def _update_status(self, status): + if status == CONNECTION_STATUS_CONNECTED: + self._connection[PROPERTIES_IFACE].Get(CONNECTION, + 'SelfHandle', + reply_handler=self.__get_self_handle_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetSelfHandle')) + self.emit('connected') + else: + for contact_handle, contact_id in self._buddy_handles.items(): + if contact_id is not None: + self.emit('buddy-removed', contact_id) + + for room_handle, activity_id in self._activity_handles.items(): + self.emit('activity-removed', activity_id) + + self._buddy_handles = {} + self._activity_handles = {} + self._buddies_per_activity = {} + self._activities_per_buddy = {} + + self.emit('disconnected') + + if status == CONNECTION_STATUS_DISCONNECTED: + self._connection = None + + def __get_self_handle_cb(self, self_handle): + self._self_handle = self_handle + + if CONNECTION_INTERFACE_CONTACT_CAPABILITIES in self._connection: + interface = CONNECTION_INTERFACE_CONTACT_CAPABILITIES + connection = self._connection[interface] + client_name = CLIENT + '.Sugar.FileTransfer' + file_transfer_channel_class = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_FILE_TRANSFER, + CHANNEL + '.TargetHandleType': HANDLE_TYPE_CONTACT} + capabilities = [] + connection.UpdateCapabilities( + [(client_name, [file_transfer_channel_class], capabilities)], + reply_handler=self.__update_capabilities_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.UpdateCapabilities')) + + connection = self._connection[CONNECTION_INTERFACE_ALIASING] + connection.connect_to_signal('AliasesChanged', + self.__aliases_changed_cb) + + connection = self._connection[CONNECTION_INTERFACE_SIMPLE_PRESENCE] + connection.connect_to_signal('PresencesChanged', + self.__presences_changed_cb) + + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.connect_to_signal('PropertiesChanged', + self.__buddy_info_updated_cb, + byte_arrays=True) + + connection.connect_to_signal('ActivitiesChanged', + self.__buddy_activities_changed_cb) + + connection.connect_to_signal('CurrentActivityChanged', + self.__current_activity_changed_cb) + home_model = shell.get_model() + home_model.connect('active-activity-changed', + self.__active_activity_changed_cb) + else: + logging.warning('Connection %s does not support OLPC buddy ' + 'properties', self._connection.object_path) + + if CONNECTION_INTERFACE_ACTIVITY_PROPERTIES in self._connection: + connection = self._connection[ + CONNECTION_INTERFACE_ACTIVITY_PROPERTIES] + connection.connect_to_signal( + 'ActivityPropertiesChanged', + self.__activity_properties_changed_cb) + else: + logging.warning('Connection %s does not support OLPC activity ' + 'properties', self._connection.object_path) + + properties = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_CONTACT_LIST, + CHANNEL + '.TargetHandleType': HANDLE_TYPE_LIST, + CHANNEL + '.TargetID': 'subscribe', + } + properties = dbus.Dictionary(properties, signature='sv') + connection = self._connection[CONNECTION_INTERFACE_REQUESTS] + is_ours, channel_path, properties = \ + connection.EnsureChannel(properties) + + channel = Channel(self._connection.service_name, channel_path) + channel[CHANNEL_INTERFACE_GROUP].connect_to_signal( + 'MembersChanged', self.__members_changed_cb) + + channel[PROPERTIES_IFACE].Get(CHANNEL_INTERFACE_GROUP, + 'Members', + reply_handler=self.__get_members_ready_cb, + error_handler=partial(self.__error_handler_cb, + 'Connection.GetMembers')) + + def __active_activity_changed_cb(self, model, home_activity): + room_handle = 0 + home_activity_id = home_activity.get_activity_id() + for handle, activity_id in self._activity_handles.items(): + if home_activity_id == activity_id: + room_handle = handle + break + if room_handle == 0: + home_activity_id = '' + + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.SetCurrentActivity( + home_activity_id, + room_handle, + reply_handler=self.__set_current_activity_cb, + error_handler=self.__set_current_activity_error_cb) + + def __set_current_activity_cb(self): + logging.warning('_Account.__set_current_activity_cb') + + def __set_current_activity_error_cb(self, error): + logging.debug('_Account.__set_current_activity__error_cb %r', error) + + def __update_capabilities_cb(self): + pass + + def __aliases_changed_cb(self, aliases): + logging.debug('_Account.__aliases_changed_cb') + for handle, alias in aliases: + if handle in self._buddy_handles: + logging.debug('Got handle %r with nick %r, going to update', + handle, alias) + properties = {CONNECTION_INTERFACE_ALIASING + '/alias': alias} + self.emit('buddy-updated', self._buddy_handles[handle], + properties) + + def __presences_changed_cb(self, presences): + logging.debug('_Account.__presences_changed_cb %r', presences) + for handle, presence in presences.iteritems(): + if handle in self._buddy_handles: + presence_type, status_, message_ = presence + if presence_type == CONNECTION_PRESENCE_TYPE_OFFLINE: + contact_id = self._buddy_handles[handle] + del self._buddy_handles[handle] + self.emit('buddy-removed', contact_id) + + def __buddy_info_updated_cb(self, handle, properties): + logging.debug('_Account.__buddy_info_updated_cb %r', handle) + self.emit('buddy-updated', self._buddy_handles[handle], properties) + + def __current_activity_changed_cb(self, contact_handle, activity_id, + room_handle): + logging.debug('_Account.__current_activity_changed_cb %r %r %r', + contact_handle, activity_id, room_handle) + if contact_handle in self._buddy_handles: + contact_id = self._buddy_handles[contact_handle] + if not activity_id and room_handle: + activity_id = self._activity_handles.get(room_handle, '') + self.emit('current-activity-updated', contact_id, activity_id) + + def __get_current_activity_cb(self, contact_handle, activity_id, + room_handle): + logging.debug('_Account.__get_current_activity_cb %r %r %r', + contact_handle, activity_id, room_handle) + + if contact_handle in self._buddy_handles: + contact_id = self._buddy_handles[contact_handle] + if not activity_id and room_handle: + activity_id = self._activity_handles.get(room_handle, '') + self.emit('current-activity-updated', contact_id, activity_id) + + def __buddy_activities_changed_cb(self, buddy_handle, activities): + self._update_buddy_activities(buddy_handle, activities) + + def _update_buddy_activities(self, buddy_handle, activities): + logging.debug('_Account._update_buddy_activities') + + if not buddy_handle in self._activities_per_buddy: + self._activities_per_buddy[buddy_handle] = set() + + for activity_id, room_handle in activities: + if room_handle not in self._activity_handles: + self._activity_handles[room_handle] = activity_id + + if buddy_handle == self._self_handle: + home_model = shell.get_model() + activity = home_model.get_active_activity() + if activity.get_activity_id() == activity_id: + connection = self._connection[ + CONNECTION_INTERFACE_BUDDY_INFO] + connection.SetCurrentActivity( + activity_id, + room_handle, + reply_handler=self.__set_current_activity_cb, + error_handler=self.__set_current_activity_error_cb) + + self.emit('activity-added', room_handle, activity_id) + + connection = self._connection[ + CONNECTION_INTERFACE_ACTIVITY_PROPERTIES] + connection.GetProperties(room_handle, + reply_handler=partial(self.__get_properties_cb, + room_handle), + error_handler=partial(self.__error_handler_cb, + 'ActivityProperties.GetProperties')) + + if buddy_handle != self._self_handle: + # Sometimes we'll get CurrentActivityChanged before we get + # to know about the activity so we miss the event. In that + # case, request again the current activity for this buddy. + connection = self._connection[ + CONNECTION_INTERFACE_BUDDY_INFO] + connection.GetCurrentActivity( + buddy_handle, + reply_handler=partial(self.__get_current_activity_cb, + buddy_handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetCurrentActivity')) + + if not activity_id in self._buddies_per_activity: + self._buddies_per_activity[activity_id] = set() + self._buddies_per_activity[activity_id].add(buddy_handle) + if activity_id not in self._activities_per_buddy[buddy_handle]: + self._activities_per_buddy[buddy_handle].add(activity_id) + if buddy_handle != self._self_handle: + self.emit('buddy-joined-activity', + self._buddy_handles[buddy_handle], + activity_id) + + current_activity_ids = \ + [activity_id for activity_id, room_handle in activities] + for activity_id in self._activities_per_buddy[buddy_handle].copy(): + if not activity_id in current_activity_ids: + self._remove_buddy_from_activity(buddy_handle, activity_id) + + def __get_properties_cb(self, room_handle, properties): + logging.debug('_Account.__get_properties_cb %r %r', room_handle, + properties) + if properties: + self._update_activity(room_handle, properties) + + def _remove_buddy_from_activity(self, buddy_handle, activity_id): + if buddy_handle in self._buddies_per_activity[activity_id]: + self._buddies_per_activity[activity_id].remove(buddy_handle) + + if activity_id in self._activities_per_buddy[buddy_handle]: + self._activities_per_buddy[buddy_handle].remove(activity_id) + + if buddy_handle != self._self_handle: + self.emit('buddy-left-activity', + self._buddy_handles[buddy_handle], + activity_id) + + if not self._buddies_per_activity[activity_id]: + del self._buddies_per_activity[activity_id] + + for room_handle in self._activity_handles.copy(): + if self._activity_handles[room_handle] == activity_id: + del self._activity_handles[room_handle] + break + + self.emit('activity-removed', activity_id) + + def __activity_properties_changed_cb(self, room_handle, properties): + logging.debug('_Account.__activity_properties_changed_cb %r %r', + room_handle, properties) + self._update_activity(room_handle, properties) + + def _update_activity(self, room_handle, properties): + if room_handle in self._activity_handles: + self.emit('activity-updated', self._activity_handles[room_handle], + properties) + else: + logging.debug('_Account.__activity_properties_changed_cb unknown ' + 'activity') + # We don't get ActivitiesChanged for the owner of the connection, + # so we query for its activities in order to find out. + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + handle = self._self_handle + connection = self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + connection.GetActivities( + handle, + reply_handler=partial(self.__got_activities_cb, handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.Getactivities')) + + def __members_changed_cb(self, message, added, removed, local_pending, + remote_pending, actor, reason): + self._add_buddy_handles(added) + + def __get_members_ready_cb(self, handles): + logging.debug('_Account.__get_members_ready_cb %r', handles) + if not handles: + return + + self._add_buddy_handles(handles) + + def _add_buddy_handles(self, handles): + logging.debug('_Account._add_buddy_handles %r', handles) + interfaces = [CONNECTION, CONNECTION_INTERFACE_ALIASING] + self._connection[CONNECTION_INTERFACE_CONTACTS].GetContactAttributes( + handles, interfaces, False, + reply_handler=self.__get_contact_attributes_cb, + error_handler=partial(self.__error_handler_cb, + 'Contacts.GetContactAttributes')) + + def __got_buddy_info_cb(self, handle, nick, properties): + logging.debug('_Account.__got_buddy_info_cb %r', handle) + self.emit('buddy-updated', self._buddy_handles[handle], properties) + + def __get_contact_attributes_cb(self, attributes): + logging.debug('_Account.__get_contact_attributes_cb %r', + attributes.keys()) + + for handle in attributes.keys(): + nick = attributes[handle][CONNECTION_INTERFACE_ALIASING + '/alias'] + + if handle == self._self_handle: + logging.debug('_Account.__get_contact_attributes_cb,' \ + ' do not add ourself %r', handle) + continue + + if handle in self._buddy_handles and \ + not self._buddy_handles[handle] is None: + logging.debug('Got handle %r with nick %r, going to update', + handle, nick) + self.emit('buddy-updated', self._buddy_handles[handle], + attributes[handle]) + else: + logging.debug('Got handle %r with nick %r, going to add', + handle, nick) + + contact_id = attributes[handle][CONNECTION + '/contact-id'] + self._buddy_handles[handle] = contact_id + + if CONNECTION_INTERFACE_BUDDY_INFO in self._connection: + connection = \ + self._connection[CONNECTION_INTERFACE_BUDDY_INFO] + + connection.GetProperties( + handle, + reply_handler=partial(self.__got_buddy_info_cb, handle, + nick), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetProperties'), + byte_arrays=True, + timeout=_QUERY_DBUS_TIMEOUT) + + connection.GetActivities( + handle, + reply_handler=partial(self.__got_activities_cb, + handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetActivities'), + timeout=_QUERY_DBUS_TIMEOUT) + + connection.GetCurrentActivity( + handle, + reply_handler=partial(self.__get_current_activity_cb, + handle), + error_handler=partial(self.__error_handler_cb, + 'BuddyInfo.GetCurrentActivity'), + timeout=_QUERY_DBUS_TIMEOUT) + + self.emit('buddy-added', contact_id, nick, handle) + + def __got_activities_cb(self, buddy_handle, activities): + logging.debug('_Account.__got_activities_cb %r %r', buddy_handle, + activities) + self._update_buddy_activities(buddy_handle, activities) + + def enable(self): + logging.debug('_Account.enable %s', self.object_path) + self._set_enabled(True) + + def disable(self): + logging.debug('_Account.disable %s', self.object_path) + self._set_enabled(False) + self._connection = None + + def _set_enabled(self, value): + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, self.object_path) + obj.Set(ACCOUNT, 'Enabled', value, + reply_handler=self.__set_enabled_cb, + error_handler=partial(self.__error_handler_cb, + 'Account.SetEnabled'), + dbus_interface=dbus.PROPERTIES_IFACE) + + def __set_enabled_cb(self): + logging.debug('_Account.__set_enabled_cb success') + + +class Neighborhood(gobject.GObject): + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + 'buddy-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([object])), + } + + def __init__(self): + gobject.GObject.__init__(self) + + self._buddies = {None: get_owner_instance()} + self._activities = {} + self._link_local_account = None + self._server_account = None + self._shell_model = shell.get_model() + + client = gconf.client_get_default() + client.add_dir('/desktop/sugar/collaboration', + gconf.CLIENT_PRELOAD_NONE) + client.notify_add('/desktop/sugar/collaboration/jabber_server', + self.__jabber_server_changed_cb) + client.add_dir('/desktop/sugar/user/nick', gconf.CLIENT_PRELOAD_NONE) + client.notify_add('/desktop/sugar/user/nick', self.__nick_changed_cb) + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_manager.Get(ACCOUNT_MANAGER, 'ValidAccounts', + dbus_interface=PROPERTIES_IFACE, + reply_handler=self.__got_accounts_cb, + error_handler=self.__error_handler_cb) + + def __got_accounts_cb(self, account_paths): + self._link_local_account = \ + self._ensure_link_local_account(account_paths) + self._connect_to_account(self._link_local_account) + + self._server_account = self._ensure_server_account(account_paths) + self._connect_to_account(self._server_account) + + def __error_handler_cb(self, error): + raise RuntimeError(error) + + def _connect_to_account(self, account): + account.connect('buddy-added', self.__buddy_added_cb) + account.connect('buddy-updated', self.__buddy_updated_cb) + account.connect('buddy-removed', self.__buddy_removed_cb) + account.connect('buddy-joined-activity', + self.__buddy_joined_activity_cb) + account.connect('buddy-left-activity', self.__buddy_left_activity_cb) + account.connect('activity-added', self.__activity_added_cb) + account.connect('activity-updated', self.__activity_updated_cb) + account.connect('activity-removed', self.__activity_removed_cb) + account.connect('current-activity-updated', + self.__current_activity_updated_cb) + account.connect('connected', self.__account_connected_cb) + account.connect('disconnected', self.__account_disconnected_cb) + + def __account_connected_cb(self, account): + logging.debug('__account_connected_cb %s', account.object_path) + if account == self._server_account: + self._link_local_account.disable() + + def __account_disconnected_cb(self, account): + logging.debug('__account_disconnected_cb %s', account.object_path) + if account == self._server_account: + self._link_local_account.enable() + + def _get_published_name(self): + """Construct the published name based on the public key + + Limit the name to be only 8 characters maximum. The avahi + service name has a 64 character limit. It consists of + the room name, the published name and the host name. + + """ + public_key_hash = sha1(get_profile().pubkey).hexdigest() + return public_key_hash[:8] + + def _ensure_link_local_account(self, account_paths): + for account_path in account_paths: + if 'salut' in account_path: + logging.debug('Already have a Salut account') + account = _Account(account_path) + account.enable() + return account + + logging.debug('Still dont have a Salut account, creating one') + + client = gconf.client_get_default() + nick = client.get_string('/desktop/sugar/user/nick') + + params = { + 'nickname': nick, + 'first-name': '', + 'last-name': '', + 'jid': self._get_jabber_account_id(), + 'published-name': self._get_published_name(), + } + + properties = { + ACCOUNT + '.Enabled': True, + ACCOUNT + '.Nickname': nick, + ACCOUNT + '.ConnectAutomatically': True, + } + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_path = account_manager.CreateAccount('salut', 'local-xmpp', + 'salut', params, + properties) + return _Account(account_path) + + def _ensure_server_account(self, account_paths): + for account_path in account_paths: + if 'gabble' in account_path: + logging.debug('Already have a Gabble account') + account = _Account(account_path) + account.enable() + return account + + logging.debug('Still dont have a Gabble account, creating one') + + client = gconf.client_get_default() + nick = client.get_string('/desktop/sugar/user/nick') + server = client.get_string('/desktop/sugar/collaboration' + '/jabber_server') + key_hash = get_profile().privkey_hash + + params = { + 'account': self._get_jabber_account_id(), + 'password': key_hash, + 'server': server, + 'resource': 'sugar', + 'require-encryption': True, + 'ignore-ssl-errors': True, + 'register': True, + 'old-ssl': True, + 'port': dbus.UInt32(5223), + } + + properties = { + ACCOUNT + '.Enabled': True, + ACCOUNT + '.Nickname': nick, + ACCOUNT + '.ConnectAutomatically': True, + } + + bus = dbus.Bus() + obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, ACCOUNT_MANAGER_PATH) + account_manager = dbus.Interface(obj, ACCOUNT_MANAGER) + account_path = account_manager.CreateAccount('gabble', 'jabber', + 'jabber', params, + properties) + return _Account(account_path) + + def _get_jabber_account_id(self): + public_key_hash = sha1(get_profile().pubkey).hexdigest() + client = gconf.client_get_default() + server = client.get_string('/desktop/sugar/collaboration' + '/jabber_server') + return '%s@%s' % (public_key_hash, server) + + def __jabber_server_changed_cb(self, client, timestamp, entry, *extra): + logging.debug('__jabber_server_changed_cb') + + bus = dbus.Bus() + account = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._server_account.object_path) + + server = client.get_string( + '/desktop/sugar/collaboration/jabber_server') + account_id = self._get_jabber_account_id() + params_needing_reconnect = account.UpdateParameters( + {'server': server, + 'account': account_id, + 'register': True}, + dbus.Array([], 's'), dbus_interface=ACCOUNT) + if params_needing_reconnect: + account.Reconnect() + + self._update_jid() + + def __nick_changed_cb(self, client, timestamp, entry, *extra): + logging.debug('__nick_changed_cb') + + nick = client.get_string('/desktop/sugar/user/nick') + + bus = dbus.Bus() + server_obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._server_account.object_path) + server_obj.Set(ACCOUNT, 'Nickname', nick, + dbus_interface=PROPERTIES_IFACE) + + link_local_obj = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._link_local_account.object_path) + link_local_obj.Set(ACCOUNT, 'Nickname', nick, + dbus_interface=PROPERTIES_IFACE) + params_needing_reconnect = link_local_obj.UpdateParameters( + {'nickname': nick, 'published-name': self._get_published_name()}, + dbus.Array([], 's'), dbus_interface=ACCOUNT) + if params_needing_reconnect: + link_local_obj.Reconnect() + + self._update_jid() + + def _update_jid(self): + bus = dbus.Bus() + account = bus.get_object(ACCOUNT_MANAGER_SERVICE, + self._link_local_account.object_path) + + account_id = self._get_jabber_account_id() + params_needing_reconnect = account.UpdateParameters( + {'jid': account_id}, dbus.Array([], 's'), dbus_interface=ACCOUNT) + if params_needing_reconnect: + account.Reconnect() + + def __buddy_added_cb(self, account, contact_id, nick, handle): + logging.debug('__buddy_added_cb %r', contact_id) + + if contact_id in self._buddies: + logging.debug('__buddy_added_cb buddy already tracked') + return + + buddy = BuddyModel( + nick=nick, + account=account.object_path, + contact_id=contact_id, + handle=handle) + self._buddies[contact_id] = buddy + + def __buddy_updated_cb(self, account, contact_id, properties): + logging.debug('__buddy_updated_cb %r', contact_id) + if contact_id is None: + # Don't know the contact-id yet, will get the full state later + return + + if contact_id not in self._buddies: + logging.debug('__buddy_updated_cb Unknown buddy with contact_id' + ' %r', contact_id) + return + + buddy = self._buddies[contact_id] + + is_new = buddy.props.key is None and 'key' in properties + + if 'color' in properties: + buddy.props.color = XoColor(properties['color']) + + if 'key' in properties: + buddy.props.key = properties['key'] + + nick_key = CONNECTION_INTERFACE_ALIASING + '/alias' + if nick_key in properties: + buddy.props.nick = properties[nick_key] + + if is_new: + self.emit('buddy-added', buddy) + + def __buddy_removed_cb(self, account, contact_id): + logging.debug('Neighborhood.__buddy_removed_cb %r', contact_id) + if contact_id not in self._buddies: + logging.debug('Neighborhood.__buddy_removed_cb Unknown buddy with ' + 'contact_id %r', contact_id) + return + + buddy = self._buddies[contact_id] + del self._buddies[contact_id] + + if buddy.props.key is not None: + self.emit('buddy-removed', buddy) + + def __activity_added_cb(self, account, room_handle, activity_id): + logging.debug('__activity_added_cb %r %r', room_handle, activity_id) + if activity_id in self._activities: + logging.debug('__activity_added_cb activity already tracked') + return + + activity = ActivityModel(activity_id, room_handle) + self._activities[activity_id] = activity + + def __activity_updated_cb(self, account, activity_id, properties): + logging.debug('__activity_updated_cb %r %r', activity_id, properties) + if activity_id not in self._activities: + logging.debug('__activity_updated_cb Unknown activity with ' + 'activity_id %r', activity_id) + return + + registry = bundleregistry.get_registry() + bundle = registry.get_bundle(properties['type']) + if not bundle: + logging.warning('Ignoring shared activity we don''t have') + return + + activity = self._activities[activity_id] + + is_new = activity.props.bundle is None + + activity.props.color = XoColor(properties['color']) + activity.props.bundle = bundle + activity.props.name = properties['name'] + activity.props.private = properties['private'] + + if is_new: + self._shell_model.add_shared_activity(activity_id, + activity.props.color) + self.emit('activity-added', activity) + + def __activity_removed_cb(self, account, activity_id): + logging.debug('__activity_removed_cb %r', activity_id) + if activity_id not in self._activities: + logging.debug('Unknown activity with id %s. Already removed?', + activity_id) + return + activity = self._activities[activity_id] + del self._activities[activity_id] + self._shell_model.remove_shared_activity(activity_id) + + if activity.props.bundle is not None: + self.emit('activity-removed', activity) + + def __current_activity_updated_cb(self, account, contact_id, activity_id): + logging.debug('__current_activity_updated_cb %r %r', contact_id, + activity_id) + if contact_id not in self._buddies: + logging.debug('__current_activity_updated_cb Unknown buddy with ' + 'contact_id %r', contact_id) + return + if activity_id and activity_id not in self._activities: + logging.debug('__current_activity_updated_cb Unknown activity with' + ' id %s', activity_id) + activity_id = '' + + buddy = self._buddies[contact_id] + if buddy.props.current_activity is not None: + if buddy.props.current_activity.activity_id == activity_id: + return + buddy.props.current_activity.remove_current_buddy(buddy) + + if activity_id: + activity = self._activities[activity_id] + buddy.props.current_activity = activity + activity.add_current_buddy(buddy) + else: + buddy.props.current_activity = None + + def __buddy_joined_activity_cb(self, account, contact_id, activity_id): + if contact_id not in self._buddies: + logging.debug('__buddy_joined_activity_cb Unknown buddy with ' + 'contact_id %r', contact_id) + return + + if activity_id not in self._activities: + logging.debug('__buddy_joined_activity_cb Unknown activity with ' + 'activity_id %r', activity_id) + return + + self._activities[activity_id].add_buddy(self._buddies[contact_id]) + + def __buddy_left_activity_cb(self, account, contact_id, activity_id): + if contact_id not in self._buddies: + logging.debug('__buddy_left_activity_cb Unknown buddy with ' + 'contact_id %r', contact_id) + return + + if activity_id not in self._activities: + logging.debug('__buddy_left_activity_cb Unknown activity with ' + 'activity_id %r', activity_id) + return + + self._activities[activity_id].remove_buddy(self._buddies[contact_id]) + + def get_buddies(self): + return self._buddies.values() + + def get_buddy_by_key(self, key): + for buddy in self._buddies.values(): + if buddy.key == key: + return buddy + return None + + def get_buddy_by_handle(self, contact_handle): + for buddy in self._buddies.values(): + if not buddy.is_owner() and buddy.handle == contact_handle: + return buddy + return None + + def get_activity(self, activity_id): + return self._activities.get(activity_id, None) + + def get_activity_by_room(self, room_handle): + for activity in self._activities.values(): + if activity.room_handle == room_handle: + return activity + return None + + def get_activities(self): + return self._activities.values() + + +def get_model(): + global _model + if _model is None: + _model = Neighborhood() + return _model diff --git a/src/jarabe/model/network.py b/src/jarabe/model/network.py new file mode 100644 index 0000000..cc02b58 --- /dev/null +++ b/src/jarabe/model/network.py @@ -0,0 +1,1096 @@ +# Copyright (C) 2008 Red Hat, Inc. +# Copyright (C) 2009 Tomeu Vizoso, Simon Schampijer +# Copyright (C) 2009-2010 One Laptop per Child +# Copyright (C) 2009 Paraguay Educa, Martin Abente +# Copyright (C) 2010 Plan Ceibal, Daniel Castelo +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from gettext import gettext as _ +import logging +import os + +import dbus +import dbus.service +import gobject +import ConfigParser +import gconf +import ctypes + +from sugar import dispatch +from sugar import env +from sugar.util import unique_id + +NM_STATE_UNKNOWN = 0 +NM_STATE_ASLEEP = 10 +NM_STATE_DISCONNECTED = 20 +NM_STATE_DISCONNECTING = 30 +NM_STATE_CONNECTING = 40 +NM_STATE_CONNECTED_LOCAL = 50 +NM_STATE_CONNECTED_SITE = 60 +NM_STATE_CONNECTED_GLOBAL = 70 + +NM_DEVICE_TYPE_UNKNOWN = 0 +NM_DEVICE_TYPE_ETHERNET = 1 +NM_DEVICE_TYPE_WIFI = 2 +NM_DEVICE_TYPE_UNUSED1 = 3 +NM_DEVICE_TYPE_UNUSED2 = 4 +NM_DEVICE_TYPE_BT = 5 +NM_DEVICE_TYPE_OLPC_MESH = 6 +NM_DEVICE_TYPE_WIMAX = 7 +NM_DEVICE_TYPE_MODEM = 8 + +NM_DEVICE_STATE_UNKNOWN = 0 +NM_DEVICE_STATE_UNMANAGED = 10 +NM_DEVICE_STATE_UNAVAILABLE = 20 +NM_DEVICE_STATE_DISCONNECTED = 30 +NM_DEVICE_STATE_PREPARE = 40 +NM_DEVICE_STATE_CONFIG = 50 +NM_DEVICE_STATE_NEED_AUTH = 60 +NM_DEVICE_STATE_IP_CONFIG = 70 +NM_DEVICE_STATE_IP_CHECK = 80 +NM_DEVICE_STATE_SECONDARIES = 90 +NM_DEVICE_STATE_ACTIVATED = 100 +NM_DEVICE_STATE_DEACTIVATING = 110 +NM_DEVICE_STATE_FAILED = 120 + +NM_CONNECTION_TYPE_802_11_WIRELESS = '802-11-wireless' +NM_CONNECTION_TYPE_GSM = 'gsm' + +NM_ACTIVE_CONNECTION_STATE_UNKNOWN = 0 +NM_ACTIVE_CONNECTION_STATE_ACTIVATING = 1 +NM_ACTIVE_CONNECTION_STATE_ACTIVATED = 2 +NM_ACTIVE_CONNECTION_STATE_DEACTIVATING = 3 + +NM_DEVICE_STATE_REASON_UNKNOWN = 0 +NM_DEVICE_STATE_REASON_NONE = 1 +NM_DEVICE_STATE_REASON_NOW_MANAGED = 2 +NM_DEVICE_STATE_REASON_NOW_UNMANAGED = 3 +NM_DEVICE_STATE_REASON_CONFIG_FAILED = 4 +NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE = 5 +NM_DEVICE_STATE_REASON_IP_CONFIG_EXPIRED = 6 +NM_DEVICE_STATE_REASON_NO_SECRETS = 7 +NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 +NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED = 9 +NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED = 10 +NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT = 11 +NM_DEVICE_STATE_REASON_PPP_START_FAILED = 12 +NM_DEVICE_STATE_REASON_PPP_DISCONNECT = 13 +NM_DEVICE_STATE_REASON_PPP_FAILED = 14 +NM_DEVICE_STATE_REASON_DHCP_START_FAILED = 15 +NM_DEVICE_STATE_REASON_DHCP_ERROR = 16 +NM_DEVICE_STATE_REASON_DHCP_FAILED = 17 +NM_DEVICE_STATE_REASON_SHARED_START_FAILED = 18 +NM_DEVICE_STATE_REASON_SHARED_FAILED = 19 +NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED = 20 +NM_DEVICE_STATE_REASON_AUTOIP_ERROR = 21 +NM_DEVICE_STATE_REASON_AUTOIP_FAILED = 22 +NM_DEVICE_STATE_REASON_MODEM_BUSY = 23 +NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE = 24 +NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER = 25 +NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT = 26 +NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED = 27 +NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED = 28 +NM_DEVICE_STATE_REASON_GSM_APN_FAILED = 29 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING = 30 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED = 31 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT = 32 +NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED = 33 +NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED = 34 +NM_DEVICE_STATE_REASON_FIRMWARE_MISSING = 35 +NM_DEVICE_STATE_REASON_REMOVED = 36 +NM_DEVICE_STATE_REASON_SLEEPING = 37 +NM_DEVICE_STATE_REASON_CONNECTION_REMOVED = 38 +NM_DEVICE_STATE_REASON_USER_REQUESTED = 39 +NM_DEVICE_STATE_REASON_CARRIER = 40 +NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED = 41 +NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE = 42 +NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND = 43 +NM_DEVICE_STATE_REASON_BT_FAILED = 44 +NM_DEVICE_STATE_REASON_LAST = 0xFFFF + +NM_802_11_AP_FLAGS_NONE = 0x00000000 +NM_802_11_AP_FLAGS_PRIVACY = 0x00000001 + +NM_802_11_AP_SEC_NONE = 0x0 +NM_802_11_AP_SEC_PAIR_WEP40 = 0x1 +NM_802_11_AP_SEC_PAIR_WEP104 = 0x2 +NM_802_11_AP_SEC_PAIR_TKIP = 0x4 +NM_802_11_AP_SEC_PAIR_CCMP = 0x8 +NM_802_11_AP_SEC_GROUP_WEP40 = 0x10 +NM_802_11_AP_SEC_GROUP_WEP104 = 0x20 +NM_802_11_AP_SEC_GROUP_TKIP = 0x40 +NM_802_11_AP_SEC_GROUP_CCMP = 0x80 +NM_802_11_AP_SEC_KEY_MGMT_PSK = 0x100 +NM_802_11_AP_SEC_KEY_MGMT_802_1X = 0x200 + +NM_802_11_MODE_UNKNOWN = 0 +NM_802_11_MODE_ADHOC = 1 +NM_802_11_MODE_INFRA = 2 + +NM_WIFI_DEVICE_CAP_NONE = 0x00000000 +NM_WIFI_DEVICE_CAP_CIPHER_WEP40 = 0x00000001 +NM_WIFI_DEVICE_CAP_CIPHER_WEP104 = 0x00000002 +NM_WIFI_DEVICE_CAP_CIPHER_TKIP = 0x00000004 +NM_WIFI_DEVICE_CAP_CIPHER_CCMP = 0x00000008 +NM_WIFI_DEVICE_CAP_WPA = 0x00000010 +NM_WIFI_DEVICE_CAP_RSN = 0x00000020 + +NM_BT_CAPABILITY_NONE = 0x00000000 +NM_BT_CAPABILITY_DUN = 0x00000001 +NM_BT_CAPABILITY_NAP = 0x00000002 + +NM_DEVICE_MODEM_CAPABILITY_NONE = 0x00000000 +NM_DEVICE_MODEM_CAPABILITY_POTS = 0x00000001 +NM_DEVICE_MODEM_CAPABILITY_CDMA_EVDO = 0x00000002 +NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS = 0x00000004 +NM_DEVICE_MODEM_CAPABILITY_LTE = 0x00000008 + +SETTINGS_SERVICE = 'org.freedesktop.NetworkManager' + +NM_SERVICE = 'org.freedesktop.NetworkManager' +NM_IFACE = 'org.freedesktop.NetworkManager' +NM_PATH = '/org/freedesktop/NetworkManager' +NM_DEVICE_IFACE = 'org.freedesktop.NetworkManager.Device' +NM_WIRED_IFACE = 'org.freedesktop.NetworkManager.Device.Wired' +NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' +NM_MODEM_IFACE = 'org.freedesktop.NetworkManager.Device.Modem' +NM_OLPC_MESH_IFACE = 'org.freedesktop.NetworkManager.Device.OlpcMesh' +NM_SETTINGS_PATH = '/org/freedesktop/NetworkManager/Settings' +NM_SETTINGS_IFACE = 'org.freedesktop.NetworkManager.Settings' +NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManager.Settings.Connection' +NM_ACCESSPOINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' +NM_ACTIVE_CONN_IFACE = 'org.freedesktop.NetworkManager.Connection.Active' + +NM_SECRET_AGENT_IFACE = 'org.freedesktop.NetworkManager.SecretAgent' +NM_SECRET_AGENT_PATH = '/org/freedesktop/NetworkManager/SecretAgent' +NM_AGENT_MANAGER_IFACE = 'org.freedesktop.NetworkManager.AgentManager' +NM_AGENT_MANAGER_PATH = '/org/freedesktop/NetworkManager/AgentManager' + +NM_AGENT_MANAGER_ERR_NO_SECRETS = 'org.freedesktop.NetworkManager.AgentManager.NoSecrets' + +GSM_CONNECTION_ID = 'Sugar Modem Connection' +GSM_BAUD_RATE = 115200 +GSM_USERNAME_PATH = '/desktop/sugar/network/gsm/username' +GSM_PASSWORD_PATH = '/desktop/sugar/network/gsm/password' +GSM_NUMBER_PATH = '/desktop/sugar/network/gsm/number' +GSM_APN_PATH = '/desktop/sugar/network/gsm/apn' +GSM_PIN_PATH = '/desktop/sugar/network/gsm/pin' +GSM_PUK_PATH = '/desktop/sugar/network/gsm/puk' + +ADHOC_CONNECTION_ID_PREFIX = 'Sugar Ad-hoc Network ' +MESH_CONNECTION_ID_PREFIX = 'OLPC Mesh Network ' +XS_MESH_CONNECTION_ID_PREFIX = 'OLPC XS Mesh Network ' + +_network_manager = None +_nm_settings = None +_secret_agent = None +_connections = None + +_nm_device_state_reason_description = None + + +def get_error_by_reason(reason): + global _nm_device_state_reason_description + + if _nm_device_state_reason_description is None: + _nm_device_state_reason_description = { + NM_DEVICE_STATE_REASON_UNKNOWN: + _('The reason for the device state change is unknown.'), + NM_DEVICE_STATE_REASON_NONE: + _('The state change is normal.'), + NM_DEVICE_STATE_REASON_NOW_MANAGED: + _('The device is now managed.'), + NM_DEVICE_STATE_REASON_NOW_UNMANAGED: + _('The device is no longer managed.'), + NM_DEVICE_STATE_REASON_CONFIG_FAILED: + _('The device could not be readied for configuration.'), + NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE: + _('IP configuration could not be reserved ' + '(no available address, timeout, etc).'), + NM_DEVICE_STATE_REASON_IP_CONFIG_EXPIRED: + _('The IP configuration is no longer valid.'), + NM_DEVICE_STATE_REASON_NO_SECRETS: + _('Secrets were required, but not provided.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT: + _('The 802.1X supplicant disconnected from ' + 'the access point or authentication server.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_CONFIG_FAILED: + _('Configuration of the 802.1X supplicant failed.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_FAILED: + _('The 802.1X supplicant quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_SUPPLICANT_TIMEOUT: + _('The 802.1X supplicant took too long to authenticate.'), + NM_DEVICE_STATE_REASON_PPP_START_FAILED: + _('The PPP service failed to start within the allowed time.'), + NM_DEVICE_STATE_REASON_PPP_DISCONNECT: + _('The PPP service disconnected unexpectedly.'), + NM_DEVICE_STATE_REASON_PPP_FAILED: + _('The PPP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_DHCP_START_FAILED: + _('The DHCP service failed to start within the allowed time.'), + NM_DEVICE_STATE_REASON_DHCP_ERROR: + _('The DHCP service reported an unexpected error.'), + NM_DEVICE_STATE_REASON_DHCP_FAILED: + _('The DHCP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_SHARED_START_FAILED: + _('The shared connection service failed to start.'), + NM_DEVICE_STATE_REASON_SHARED_FAILED: + _('The shared connection service quit or failed' + ' unexpectedly.'), + NM_DEVICE_STATE_REASON_AUTOIP_START_FAILED: + _('The AutoIP service failed to start.'), + NM_DEVICE_STATE_REASON_AUTOIP_ERROR: + _('The AutoIP service reported an unexpected error.'), + NM_DEVICE_STATE_REASON_AUTOIP_FAILED: + _('The AutoIP service quit or failed unexpectedly.'), + NM_DEVICE_STATE_REASON_MODEM_BUSY: + _('Dialing failed because the line was busy.'), + NM_DEVICE_STATE_REASON_MODEM_NO_DIAL_TONE: + _('Dialing failed because there was no dial tone.'), + NM_DEVICE_STATE_REASON_MODEM_NO_CARRIER: + _('Dialing failed because there was no carrier.'), + NM_DEVICE_STATE_REASON_MODEM_DIAL_TIMEOUT: + _('Dialing timed out.'), + NM_DEVICE_STATE_REASON_MODEM_DIAL_FAILED: + _('Dialing failed.'), + NM_DEVICE_STATE_REASON_MODEM_INIT_FAILED: + _('Modem initialization failed.'), + NM_DEVICE_STATE_REASON_GSM_APN_FAILED: + _('Failed to select the specified GSM APN'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_NOT_SEARCHING: + _('Not searching for networks.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_DENIED: + _('Network registration was denied.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_TIMEOUT: + _('Network registration timed out.'), + NM_DEVICE_STATE_REASON_GSM_REGISTRATION_FAILED: + _('Failed to register with the requested GSM network.'), + NM_DEVICE_STATE_REASON_GSM_PIN_CHECK_FAILED: + _('PIN check failed.'), + NM_DEVICE_STATE_REASON_FIRMWARE_MISSING: + _('Necessary firmware for the device may be missing.'), + NM_DEVICE_STATE_REASON_REMOVED: + _('The device was removed.'), + NM_DEVICE_STATE_REASON_SLEEPING: + _('NetworkManager went to sleep.'), + NM_DEVICE_STATE_REASON_CONNECTION_REMOVED: + _("The device's active connection was removed " + "or disappeared."), + NM_DEVICE_STATE_REASON_USER_REQUESTED: + _('A user or client requested the disconnection.'), + NM_DEVICE_STATE_REASON_CARRIER: + _("The device's carrier/link changed."), + NM_DEVICE_STATE_REASON_CONNECTION_ASSUMED: + _("The device's existing connection was assumed."), + NM_DEVICE_STATE_REASON_SUPPLICANT_AVAILABLE: + _("The supplicant is now available."), + NM_DEVICE_STATE_REASON_MODEM_NOT_FOUND: + _("The modem could not be found."), + NM_DEVICE_STATE_REASON_BT_FAILED: + _("The Bluetooth connection failed or timed out."), + NM_DEVICE_STATE_REASON_LAST: + _("Unused."), + } + + return _nm_device_state_reason_description[reason] + + +def frequency_to_channel(frequency): + """Returns the channel matching a given radio channel frequency. If a + frequency is not in the dictionary channel 1 will be returned. + + Keyword arguments: + frequency -- The radio channel frequency in MHz. + + Return: Channel + + """ + ftoc = {2412: 1, 2417: 2, 2422: 3, 2427: 4, + 2432: 5, 2437: 6, 2442: 7, 2447: 8, + 2452: 9, 2457: 10, 2462: 11, 2467: 12, + 2472: 13} + if frequency not in ftoc: + logging.warning('The frequency %s can not be mapped to a channel, ' + 'defaulting to channel 1.', frequency) + return 1 + return ftoc[frequency] + + +def is_sugar_adhoc_network(ssid): + """Checks whether an access point is a sugar Ad-hoc network. + + Keyword arguments: + ssid -- Ssid of the access point. + + Return: Boolean + + """ + return ssid.startswith('Ad-hoc Network') + + +class WirelessSecurity(object): + def __init__(self): + self.key_mgmt = None + self.proto = None + self.group = None + self.pairwise = None + self.wep_key = None + self.psk = None + self.auth_alg = None + + def get_dict(self): + wireless_security = {} + if self.key_mgmt is not None: + wireless_security['key-mgmt'] = self.key_mgmt + if self.proto is not None: + wireless_security['proto'] = self.proto + if self.pairwise is not None: + wireless_security['pairwise'] = self.pairwise + if self.group is not None: + wireless_security['group'] = self.group + if self.wep_key is not None: + wireless_security['wep-key0'] = self.wep_key + if self.psk is not None: + wireless_security['psk'] = self.psk + if self.auth_alg is not None: + wireless_security['auth-alg'] = self.auth_alg + return wireless_security + + +class Wireless(object): + nm_name = '802-11-wireless' + + def __init__(self): + self.ssid = None + self.security = None + self.mode = None + self.band = None + self.channel = None + + def get_dict(self): + wireless = {'ssid': self.ssid} + if self.security: + wireless['security'] = self.security + if self.mode: + wireless['mode'] = self.mode + if self.band: + wireless['band'] = self.band + if self.channel: + wireless['channel'] = self.channel + return wireless + + +class OlpcMesh(object): + nm_name = '802-11-olpc-mesh' + + def __init__(self, channel, anycast_addr): + self.channel = channel + self.anycast_addr = anycast_addr + + def get_dict(self): + ret = { + 'ssid': dbus.ByteArray('olpc-mesh'), + 'channel': self.channel, + } + + if self.anycast_addr: + ret['dhcp-anycast-address'] = dbus.ByteArray(self.anycast_addr) + return ret + + +class ConnectionSettings(object): + def __init__(self): + self.id = None + self.uuid = None + self.type = None + self.autoconnect = False + self.timestamp = None + + def get_dict(self): + connection = {'id': self.id, + 'uuid': self.uuid, + 'type': self.type, + 'autoconnect': self.autoconnect} + if self.timestamp: + connection['timestamp'] = self.timestamp + return connection + + +class IP4Config(object): + def __init__(self): + self.method = None + + def get_dict(self): + ip4_config = {} + if self.method is not None: + ip4_config['method'] = self.method + return ip4_config + + +class Serial(object): + def __init__(self): + self.baud = None + + def get_dict(self): + serial = {} + + if self.baud is not None: + serial['baud'] = self.baud + + return serial + + +class Ppp(object): + def __init__(self): + pass + + def get_dict(self): + ppp = {} + return ppp + + +class Gsm(object): + def __init__(self): + self.apn = None + self.number = None + self.username = None + self.pin = None + self.password = None + + def get_dict(self): + gsm = {} + + if self.apn: + gsm['apn'] = self.apn + if self.number: + gsm['number'] = self.number + if self.username: + gsm['username'] = self.username + if self.password: + gsm['password'] = self.password + if self.pin: + gsm['pin'] = self.pin + + return gsm + + +class Settings(object): + def __init__(self, wireless_cfg=None): + self.connection = ConnectionSettings() + self.ip4_config = None + self.wireless_security = None + + if wireless_cfg is not None: + self.wireless = wireless_cfg + else: + self.wireless = Wireless() + + def get_dict(self): + settings = {} + settings['connection'] = self.connection.get_dict() + settings[self.wireless.nm_name] = self.wireless.get_dict() + if self.wireless_security is not None: + settings['802-11-wireless-security'] = \ + self.wireless_security.get_dict() + if self.ip4_config is not None: + settings['ipv4'] = self.ip4_config.get_dict() + return settings + + +class SettingsGsm(object): + def __init__(self): + self.connection = ConnectionSettings() + self.ip4_config = IP4Config() + self.serial = Serial() + self.ppp = Ppp() + self.gsm = Gsm() + + def get_dict(self): + settings = {} + + settings['connection'] = self.connection.get_dict() + settings['serial'] = self.serial.get_dict() + settings['ppp'] = self.ppp.get_dict() + settings['gsm'] = self.gsm.get_dict() + settings['ipv4'] = self.ip4_config.get_dict() + + return settings + + +class SecretsResponse(object): + """Intermediate object to report the secrets from the dialog + back to the connection object and which will inform NM + """ + def __init__(self, reply_cb, error_cb): + self._reply_cb = reply_cb + self._error_cb = error_cb + + def set_secrets(self, secrets): + self._reply_cb(secrets) + + def set_error(self, error): + self._error_cb(error) + + +def set_connected(): + try: + # try to flush resolver cache - SL#1940 + # ctypes' syntactic sugar does not work + # so we must get the func ptr explicitly + libc = ctypes.CDLL('libc.so.6') + res_init = getattr(libc, '__res_init') + res_init(None) + except: + # pylint: disable=W0702 + logging.exception('Error calling libc.__res_init') + + +class SecretAgent(dbus.service.Object): + def __init__(self): + self._bus = dbus.SystemBus() + dbus.service.Object.__init__(self, self._bus, NM_SECRET_AGENT_PATH) + self.secrets_request = dispatch.Signal() + proxy = self._bus.get_object(NM_IFACE, NM_AGENT_MANAGER_PATH) + proxy.Register("org.sugarlabs.sugar", + dbus_interface=NM_AGENT_MANAGER_IFACE, + reply_handler=self._register_reply_cb, + error_handler=self._register_error_cb) + + def _register_reply_cb(self): + logging.debug("SecretAgent registered") + + def _register_error_cb(self, error): + logging.error("Failed to register SecretAgent: %s", error) + + @dbus.service.method(NM_SECRET_AGENT_IFACE, + async_callbacks=('reply', 'error'), + in_signature='a{sa{sv}}osasb', + out_signature='a{sa{sv}}', + sender_keyword='sender', + byte_arrays=True) + def GetSecrets(self, settings, connection_path, setting_name, hints, + request_new, reply, error, sender=None): + if setting_name != '802-11-wireless-security': + raise ValueError("Unsupported setting type %s" % (setting_name,)) + if not sender: + raise Exception("Internal error: couldn't get sender") + uid = self._bus.get_unix_user(sender) + if uid != 0: + raise Exception("UID %d not authorized" % (uid,)) + + response = SecretsResponse(reply, error) + self.secrets_request.send(self, settings=settings, response=response) + + +class AccessPoint(gobject.GObject): + __gsignals__ = { + 'props-changed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + } + + def __init__(self, device, model): + self.__gobject_init__() + self.device = device + self.model = model + + self._initialized = False + self._bus = dbus.SystemBus() + + self.ssid = '' + self.strength = 0 + self.flags = 0 + self.wpa_flags = 0 + self.rsn_flags = 0 + self.mode = 0 + self.channel = 0 + + def initialize(self): + model_props = dbus.Interface(self.model, dbus.PROPERTIES_IFACE) + model_props.GetAll(NM_ACCESSPOINT_IFACE, byte_arrays=True, + reply_handler=self._ap_properties_changed_cb, + error_handler=self._get_all_props_error_cb) + + self._bus.add_signal_receiver(self._ap_properties_changed_cb, + signal_name='PropertiesChanged', + path=self.model.object_path, + dbus_interface=NM_ACCESSPOINT_IFACE, + byte_arrays=True) + + def network_hash(self): + """ + This is a hash which uniquely identifies the network that this AP + is a bridge to. i.e. its expected for 2 APs with identical SSID and + other settings to have the same network hash, because we assume that + they are a part of the same underlying network. + """ + + # based on logic from nm-applet + fl = 0 + + if self.mode == NM_802_11_MODE_INFRA: + fl |= 1 << 0 + elif self.mode == NM_802_11_MODE_ADHOC: + fl |= 1 << 1 + else: + fl |= 1 << 2 + + # Separate out no encryption, WEP-only, and WPA-capable */ + if (not (self.flags & NM_802_11_AP_FLAGS_PRIVACY)) \ + and self.wpa_flags == NM_802_11_AP_SEC_NONE \ + and self.rsn_flags == NM_802_11_AP_SEC_NONE: + fl |= 1 << 3 + elif (self.flags & NM_802_11_AP_FLAGS_PRIVACY) \ + and self.wpa_flags == NM_802_11_AP_SEC_NONE \ + and self.rsn_flags == NM_802_11_AP_SEC_NONE: + fl |= 1 << 4 + elif (not (self.flags & NM_802_11_AP_FLAGS_PRIVACY)) \ + and self.wpa_flags != NM_802_11_AP_SEC_NONE \ + and self.rsn_flags != NM_802_11_AP_SEC_NONE: + fl |= 1 << 5 + else: + fl |= 1 << 6 + + hashstr = str(fl) + '@' + self.ssid + return hash(hashstr) + + def _update_properties(self, properties): + if self._initialized: + old_hash = self.network_hash() + else: + old_hash = None + + if 'Ssid' in properties: + self.ssid = properties['Ssid'] + if 'Strength' in properties: + self.strength = properties['Strength'] + if 'Flags' in properties: + self.flags = properties['Flags'] + if 'WpaFlags' in properties: + self.wpa_flags = properties['WpaFlags'] + if 'RsnFlags' in properties: + self.rsn_flags = properties['RsnFlags'] + if 'Mode' in properties: + self.mode = properties['Mode'] + if 'Frequency' in properties: + self.channel = frequency_to_channel(properties['Frequency']) + + self._initialized = True + self.emit('props-changed', old_hash) + + def _get_all_props_error_cb(self, err): + logging.error('Error getting the access point properties: %s', err) + + def _ap_properties_changed_cb(self, properties): + self._update_properties(properties) + + def disconnect(self): + self._bus.remove_signal_receiver(self._ap_properties_changed_cb, + signal_name='PropertiesChanged', + path=self.model.object_path, + dbus_interface=NM_ACCESSPOINT_IFACE) + + +def get_manager(): + global _network_manager + if _network_manager is None: + obj = dbus.SystemBus().get_object(NM_SERVICE, NM_PATH) + _network_manager = dbus.Interface(obj, NM_IFACE) + return _network_manager + + +def _get_settings(): + global _nm_settings + if _nm_settings is None: + obj = dbus.SystemBus().get_object(NM_SERVICE, NM_SETTINGS_PATH) + _nm_settings = dbus.Interface(obj, NM_SETTINGS_IFACE) + _migrate_old_wifi_connections() + _migrate_old_gsm_connection() + return _nm_settings + + +def get_secret_agent(): + global _secret_agent + if _secret_agent is None: + _secret_agent = SecretAgent() + return _secret_agent + + +def _activate_reply_cb(connection_path): + logging.debug('Activated connection: %s', connection_path) + + +def _activate_error_cb(err): + logging.error('Failed to activate connection: %s', err) + + +def _add_and_activate_reply_cb(settings_path, connection_path): + logging.debug('Added and activated connection: %s', connection_path) + + +def _add_and_activate_error_cb(err): + logging.error('Failed to add and activate connection: %s', err) + + +class Connection(gobject.GObject): + __gsignals__ = { + 'removed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + } + + def __init__(self, bus, path): + gobject.GObject.__init__(self) + obj = bus.get_object(NM_SERVICE, path) + self._connection = dbus.Interface(obj, NM_CONNECTION_IFACE) + self._removed_handle = self._connection.connect_to_signal( + 'Removed', self._removed_cb) + self._updated_handle = self._connection.connect_to_signal( + 'Updated', self._updated_cb) + self._settings = self._connection.GetSettings(byte_arrays=True) + + def _updated_cb(self): + self._settings = self._connection.GetSettings(byte_arrays=True) + + def _removed_cb(self): + self._updated_handle.remove() + self._removed_handle.remove() + self.emit('removed') + + def get_settings(self, stype=None): + if not stype: + return self._settings + elif stype in self._settings: + return self._settings[stype] + else: + return None + + def get_secrets(self, stype, reply_handler, error_handler): + return self._connection.GetSecrets(stype, byte_arrays=True, + reply_handler=reply_handler, + error_handler=error_handler) + + def update_settings(self, settings): + self._connection.Update(settings) + + def activate(self, device_o, reply_handler=_activate_reply_cb, + error_handler=_activate_error_cb): + activate_connection_by_path(self.get_path(), device_o, + reply_handler=reply_handler, + error_handler=error_handler) + + def delete(self): + self._connection.Delete() + + def get_ssid(self): + wifi_settings = self.get_settings('802-11-wireless') + if wifi_settings and 'ssid' in wifi_settings: + return wifi_settings['ssid'] + else: + return None + + def get_id(self): + return self.get_settings('connection')['id'] + + def get_path(self): + return self._connection.object_path + + def is_sugar_internal_connection(self): + """Returns True if this connection is a 'special' Sugar connection, + i.e. one that has been created by Sugar internals and should not be + visible to the user or deleted by connection-clearing code.""" + connection_id = self.get_id() + return connection_id == GSM_CONNECTION_ID \ + or connection_id.startswith(ADHOC_CONNECTION_ID_PREFIX) \ + or connection_id.startswith(MESH_CONNECTION_ID_PREFIX) \ + or connection_id.startswith(XS_MESH_CONNECTION_ID_PREFIX) + + +class Connections(object): + def __init__(self): + self._bus = dbus.SystemBus() + self._connections = [] + + settings = _get_settings() + settings.connect_to_signal('NewConnection', self._new_connection_cb) + + for connection_o in settings.ListConnections(): + self._monitor_connection(connection_o) + + def get_list(self): + return self._connections + + def _monitor_connection(self, connection_o): + connection = Connection(self._bus, connection_o) + connection.connect('removed', self._connection_removed_cb) + self._connections.append(connection) + + def _new_connection_cb(self, connection_o): + self._monitor_connection(connection_o) + + def _connection_removed_cb(self, connection): + connection.disconnect_by_func(self._connection_removed_cb) + self._connections.remove(connection) + + def clear(self): + """Remove all connections except Sugar-internal ones.""" + + # copy the list, to avoid problems with removing elements of a list + # while looping over it + connections = list(self._connections) + for connection in connections: + if connection.is_sugar_internal_connection(): + continue + try: + connection.delete() + except dbus.DBusException: + logging.debug("Could not remove connection %s", + connection.get_id()) + + +def get_connections(): + global _connections + if _connections is None: + _connections = Connections() + return _connections + + +def find_connection_by_ssid(ssid): + # FIXME: this check should be more extensive. + # it should look at mode (infra/adhoc), band, security, and really + # anything that is stored in the settings. + for connection in get_connections().get_list(): + if connection.get_ssid() == ssid: + return connection + return None + + +def find_connection_by_id(connection_id): + for connection in get_connections().get_list(): + if connection.get_id() == connection_id: + return connection + return None + + +def _add_connection_reply_cb(connection): + logging.debug('Added connection: %s', connection) + + +def _add_connection_error_cb(err): + logging.error('Failed to add connection: %s', err) + + +def add_connection(settings, reply_handler=_add_connection_reply_cb, + error_handler=_add_connection_error_cb): + _get_settings().AddConnection(settings.get_dict(), + reply_handler=reply_handler, + error_handler=error_handler) + + +def activate_connection_by_path(connection, device_o, + reply_handler=_activate_reply_cb, + error_handler=_activate_error_cb): + get_manager().ActivateConnection(connection, + device_o, + '/', + reply_handler=reply_handler, + error_handler=error_handler) + + +def add_and_activate_connection(device_o, settings, specific_object): + manager = get_manager() + manager.AddAndActivateConnection(settings.get_dict(), device_o, + specific_object, + reply_handler=_add_and_activate_reply_cb, + error_handler=_add_and_activate_error_cb) + + +def _migrate_old_wifi_connections(): + """Migrate connections.cfg from Sugar-0.94 and previous to NetworkManager + system-wide connections + """ + + profile_path = env.get_profile_path() + config_path = os.path.join(profile_path, 'nm', 'connections.cfg') + if not os.path.exists(config_path): + return + + config = ConfigParser.ConfigParser() + try: + if not config.read(config_path): + logging.error('Error reading the nm config file') + return + except ConfigParser.ParsingError: + logging.exception('Error reading the nm config file') + return + + for section in config.sections(): + try: + settings = Settings() + settings.connection.id = section + ssid = config.get(section, 'ssid') + settings.wireless.ssid = dbus.ByteArray(ssid) + uuid = config.get(section, 'uuid') + settings.connection.uuid = uuid + nmtype = config.get(section, 'type') + settings.connection.type = nmtype + autoconnect = bool(config.get(section, 'autoconnect')) + settings.connection.autoconnect = autoconnect + + if config.has_option(section, 'timestamp'): + timestamp = int(config.get(section, 'timestamp')) + settings.connection.timestamp = timestamp + + if config.has_option(section, 'key-mgmt'): + settings.wireless_security = WirelessSecurity() + mgmt = config.get(section, 'key-mgmt') + settings.wireless_security.key_mgmt = mgmt + security = config.get(section, 'security') + settings.wireless.security = security + key = config.get(section, 'key') + if mgmt == 'none': + settings.wireless_security.wep_key = key + auth_alg = config.get(section, 'auth-alg') + settings.wireless_security.auth_alg = auth_alg + elif mgmt == 'wpa-psk': + settings.wireless_security.psk = key + if config.has_option(section, 'proto'): + value = config.get(section, 'proto') + settings.wireless_security.proto = value + if config.has_option(section, 'group'): + value = config.get(section, 'group') + settings.wireless_security.group = value + if config.has_option(section, 'pairwise'): + value = config.get(section, 'pairwise') + settings.wireless_security.pairwise = value + except ConfigParser.Error: + logging.exception('Error reading section') + else: + add_connection(settings) + + os.unlink(config_path) + + +def create_gsm_connection(username, password, number, apn, pin): + settings = SettingsGsm() + settings.gsm.username = username + settings.gsm.number = number + settings.gsm.apn = apn + settings.gsm.pin = pin + settings.gsm.password = password + + settings.connection.id = GSM_CONNECTION_ID + settings.connection.type = NM_CONNECTION_TYPE_GSM + settings.connection.uuid = unique_id() + settings.connection.autoconnect = False + settings.ip4_config.method = 'auto' + settings.serial.baud = GSM_BAUD_RATE + + add_connection(settings) + + +def _migrate_old_gsm_connection(): + if find_gsm_connection(): + # don't attempt migration if a NM-level connection already exists + return + + client = gconf.client_get_default() + + username = client.get_string(GSM_USERNAME_PATH) or '' + password = client.get_string(GSM_PASSWORD_PATH) or '' + number = client.get_string(GSM_NUMBER_PATH) or '' + apn = client.get_string(GSM_APN_PATH) or '' + pin = client.get_string(GSM_PIN_PATH) or '' + + if apn or number: + logging.info("Migrating old GSM connection details") + try: + create_gsm_connection(username, password, number, apn, pin) + # remove old connection + for setting in (GSM_USERNAME_PATH, GSM_PASSWORD_PATH, + GSM_NUMBER_PATH, GSM_APN_PATH, GSM_PIN_PATH, + GSM_PUK_PATH): + client.set_string(setting, '') + except Exception: + logging.exception('Error adding gsm connection to NMSettings.') + + +def find_gsm_connection(): + return find_connection_by_id(GSM_CONNECTION_ID) + + +def disconnect_access_points(ap_paths): + """ + Disconnect all devices connected to any of the given access points. + """ + bus = dbus.SystemBus() + netmgr_obj = bus.get_object(NM_SERVICE, NM_PATH) + netmgr = dbus.Interface(netmgr_obj, NM_IFACE) + netmgr_props = dbus.Interface(netmgr, dbus.PROPERTIES_IFACE) + active_connection_paths = netmgr_props.Get(NM_IFACE, 'ActiveConnections') + + for conn_path in active_connection_paths: + conn_obj = bus.get_object(NM_IFACE, conn_path) + conn_props = dbus.Interface(conn_obj, dbus.PROPERTIES_IFACE) + ap_path = conn_props.Get(NM_ACTIVE_CONN_IFACE, 'SpecificObject') + if ap_path == '/' or ap_path not in ap_paths: + continue + + dev_paths = conn_props.Get(NM_ACTIVE_CONN_IFACE, 'Devices') + for dev_path in dev_paths: + dev_obj = bus.get_object(NM_SERVICE, dev_path) + dev = dbus.Interface(dev_obj, NM_DEVICE_IFACE) + dev.Disconnect() + + +def _is_non_printable(char): + """ + Return True if char is a non-printable unicode character, False otherwise + """ + return (char < u' ') or (u'~' < char < u'\xA0') or (char == u'\xAD') + + +def ssid_to_display_name(ssid): + """Convert an SSID into a unicode string for recognising Access Points + + Return a unicode string that's useful for recognising and + distinguishing between Access Points (APs). + + IEEE 802.11 defines SSIDs as arbitrary byte sequences. As random + bytes are not very user-friendly, most APs use some human-readable + character string as SSID. However, because there's no standard + specifying what encoding to use, AP vendors chose various + different encodings. Since there's also no indication of what + encoding was used for a particular SSID, the best we can do for + turning an SSID into a displayable string is to try a couple of + encodings based on some heuristic. + + We're currently using the following heuristic: + + 1. If the SSID is a valid character string consisting only of + printable characters in one of the following encodings (tried in + the given order), decode it accordingly: + UTF-8, ISO-8859-1, Windows-1251. + 2. Return a hex dump of the SSID. + """ + for encoding in ['utf-8', 'iso-8859-1', 'windows-1251']: + try: + display_name = unicode(ssid, encoding) + except UnicodeDecodeError: + continue + + if not [True for char in display_name if _is_non_printable(char)]: + # Only printable characters + return display_name + + return ':'.join(['%02x' % (ord(byte), ) for byte in ssid]) diff --git a/src/jarabe/model/notifications.py b/src/jarabe/model/notifications.py new file mode 100644 index 0000000..ec14056 --- /dev/null +++ b/src/jarabe/model/notifications.py @@ -0,0 +1,98 @@ +# Copyright (C) 2008 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import sys +import logging + +import dbus + +from sugar import dispatch + +from jarabe import config + + +_DBUS_SERVICE = 'org.freedesktop.Notifications' +_DBUS_IFACE = 'org.freedesktop.Notifications' +_DBUS_PATH = '/org/freedesktop/Notifications' + +_instance = None + + +class NotificationService(dbus.service.Object): + def __init__(self): + bus = dbus.SessionBus() + bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus) + dbus.service.Object.__init__(self, bus_name, _DBUS_PATH) + + self._notification_counter = 0 + self.notification_received = dispatch.Signal() + self.notification_cancelled = dispatch.Signal() + + @dbus.service.method(_DBUS_IFACE, + in_signature='susssava{sv}i', out_signature='u') + def Notify(self, app_name, replaces_id, app_icon, summary, body, actions, + hints, expire_timeout): + + logging.debug('Received notification: %r', [app_name, replaces_id, + '<app_icon>', summary, body, actions, '<hints>', + expire_timeout]) + + if replaces_id > 0: + notification_id = replaces_id + else: + if self._notification_counter == sys.maxint: + self._notification_counter = 1 + else: + self._notification_counter += 1 + notification_id = self._notification_counter + + self.notification_received.send(self, app_name=app_name, + replaces_id=replaces_id, app_icon=app_icon, summary=summary, + body=body, actions=actions, hints=hints, + expire_timeout=expire_timeout) + + return notification_id + + @dbus.service.method(_DBUS_IFACE, in_signature='u', out_signature='') + def CloseNotification(self, notification_id): + self.notification_cancelled.send(self, notification_id=notification_id) + + @dbus.service.method(_DBUS_IFACE, in_signature='', out_signature='as') + def GetCapabilities(self): + return [] + + @dbus.service.method(_DBUS_IFACE, in_signature='', out_signature='sss') + def GetServerInformation(self, name, vendor, version): + return 'Sugar Shell', 'Sugar', config.version + + @dbus.service.signal(_DBUS_IFACE, signature='uu') + def NotificationClosed(self, notification_id, reason): + pass + + @dbus.service.signal(_DBUS_IFACE, signature='us') + def ActionInvoked(self, notification_id, action_key): + pass + + +def get_service(): + global _instance + if not _instance: + _instance = NotificationService() + return _instance + + +def init(): + get_service() diff --git a/src/jarabe/model/olpcmesh.py b/src/jarabe/model/olpcmesh.py new file mode 100644 index 0000000..6ab7ab6 --- /dev/null +++ b/src/jarabe/model/olpcmesh.py @@ -0,0 +1,228 @@ +# Copyright (C) 2009, 2010 One Laptop per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +import gobject + +from jarabe.model import network +from jarabe.model.network import Settings +from jarabe.model.network import OlpcMesh as OlpcMeshSettings +from sugar.util import unique_id + +_XS_ANYCAST = '\xc0\x27\xc0\x27\xc0\x00' + + +class OlpcMeshManager(object): + def __init__(self, mesh_device): + self._bus = dbus.SystemBus() + + # counter for how many asynchronous connection additions we are + # waiting for + self._add_connections_pending = 0 + + self.mesh_device = mesh_device + self.eth_device = self._get_companion_device() + + self._connection_queue = [] + """Stack of connections that we'll iterate through until we find one + that works. Each entry in the list specifies the channel and + whether to seek an XS or not.""" + + # Ensure that all the connections we'll use later are present + for channel in (1, 6, 11): + self._ensure_connection_exists(channel, xs_hosted=True) + self._ensure_connection_exists(channel, xs_hosted=False) + + props = dbus.Interface(self.mesh_device, dbus.PROPERTIES_IFACE) + props.Get(network.NM_DEVICE_IFACE, 'State', + reply_handler=self.__get_mesh_state_reply_cb, + error_handler=self.__get_state_error_cb) + + props = dbus.Interface(self.eth_device, dbus.PROPERTIES_IFACE) + props.Get(network.NM_DEVICE_IFACE, 'State', + reply_handler=self.__get_eth_state_reply_cb, + error_handler=self.__get_state_error_cb) + + self._bus.add_signal_receiver(self.__eth_device_state_changed_cb, + signal_name='StateChanged', + path=self.eth_device.object_path, + dbus_interface=network.NM_DEVICE_IFACE) + + self._bus.add_signal_receiver(self.__mshdev_state_changed_cb, + signal_name='StateChanged', + path=self.mesh_device.object_path, + dbus_interface=network.NM_DEVICE_IFACE) + + self._idle_source = 0 + self._mesh_device_state = network.NM_DEVICE_STATE_UNKNOWN + self._eth_device_state = network.NM_DEVICE_STATE_UNKNOWN + + if self._add_connections_pending == 0: + self.ready() + + def ready(self): + """Called when all connections have been added (if they were not + already present), meaning that we can start the automesh functionality. + """ + if self._have_configured_connections(): + self._start_automesh_timer() + else: + self._start_automesh() + + def _get_companion_device(self): + props = dbus.Interface(self.mesh_device, dbus.PROPERTIES_IFACE) + eth_device_o = props.Get(network.NM_OLPC_MESH_IFACE, 'Companion') + return self._bus.get_object(network.NM_SERVICE, eth_device_o) + + def _have_configured_connections(self): + return len(network.get_connections().get_list()) > 0 + + def _start_automesh_timer(self): + """Start our timer system which basically looks for 10 seconds of + inactivity on both devices, then starts automesh. + + """ + if self._idle_source != 0: + gobject.source_remove(self._idle_source) + self._idle_source = gobject.timeout_add_seconds(10, self._idle_check) + + def __get_state_error_cb(self, err): + logging.debug('Error getting the device state: %s', err) + + def __get_mesh_state_reply_cb(self, state): + self._mesh_device_state = state + self._maybe_schedule_idle_check() + + def __get_eth_state_reply_cb(self, state): + self._eth_device_state = state + self._maybe_schedule_idle_check() + + def __eth_device_state_changed_cb(self, new_state, old_state, reason): + """If a connection is activated on the eth device, stop trying our + automatic connections. + + """ + self._eth_device_state = new_state + self._maybe_schedule_idle_check() + + if new_state >= network.NM_DEVICE_STATE_PREPARE \ + and new_state <= network.NM_DEVICE_STATE_ACTIVATED \ + and len(self._connection_queue) > 0: + self._connection_queue = [] + + def __mshdev_state_changed_cb(self, new_state, old_state, reason): + self._mesh_device_state = new_state + self._maybe_schedule_idle_check() + + if new_state == network.NM_DEVICE_STATE_FAILED: + self._try_next_connection_from_queue() + elif new_state == network.NM_DEVICE_STATE_ACTIVATED \ + and len(self._connection_queue) > 0: + self._empty_connection_queue() + + def _maybe_schedule_idle_check(self): + if self._mesh_device_state == network.NM_DEVICE_STATE_DISCONNECTED \ + and self._eth_device_state == network.NM_DEVICE_STATE_DISCONNECTED: + self._start_automesh_timer() + + def _idle_check(self): + if self._mesh_device_state == network.NM_DEVICE_STATE_DISCONNECTED \ + and self._eth_device_state == network.NM_DEVICE_STATE_DISCONNECTED: + logging.debug('starting automesh due to inactivity') + self._start_automesh() + return False + + @staticmethod + def _get_connection_id(channel, xs_hosted): + if xs_hosted: + return '%s%d' % (network.XS_MESH_CONNECTION_ID_PREFIX, channel) + else: + return '%s%d' % (network.MESH_CONNECTION_ID_PREFIX, channel) + + def _connection_added(self): + if self._add_connections_pending > 0: + self._add_connections_pending = self._add_connections_pending - 1 + if self._add_connections_pending == 0: + self.ready() + + def _add_connection_reply_cb(self, connection): + logging.debug("Added connection: %s", connection) + self._connection_added() + + def _add_connection_err_cb(self, err): + logging.debug("Error adding mesh connection: %s", err) + self._connection_added() + + def _add_connection(self, channel, xs_hosted): + anycast_addr = _XS_ANYCAST if xs_hosted else None + wireless_config = OlpcMeshSettings(channel, anycast_addr) + settings = Settings(wireless_cfg=wireless_config) + if not xs_hosted: + settings.ip4_config = network.IP4Config() + settings.ip4_config.method = 'link-local' + settings.connection.id = self._get_connection_id(channel, xs_hosted) + settings.connection.autoconnect = False + settings.connection.uuid = unique_id() + settings.connection.type = '802-11-olpc-mesh' + network.add_connection(settings, + reply_handler=self._add_connection_reply_cb, + error_handler=self._add_connection_err_cb) + + def _find_connection(self, channel, xs_hosted): + connection_id = self._get_connection_id(channel, xs_hosted) + return network.find_connection_by_id(connection_id) + + def _ensure_connection_exists(self, channel, xs_hosted): + if not self._find_connection(channel, xs_hosted): + self._add_connection(channel, xs_hosted) + + def _activate_connection(self, channel, xs_hosted): + connection = self._find_connection(channel, xs_hosted) + if connection: + connection.activate(self.mesh_device.object_path) + else: + logging.warning("Could not find mesh connection") + + def _try_next_connection_from_queue(self): + if len(self._connection_queue) == 0: + return + + channel, xs_hosted = self._connection_queue.pop() + self._activate_connection(channel, xs_hosted) + + def _empty_connection_queue(self): + self._connection_queue = [] + + def user_activate_channel(self, channel): + """Activate a mesh connection on a user-specified channel. + Looks for XS first, then resorts to simple mesh.""" + self._empty_connection_queue() + self._connection_queue.append((channel, False)) + self._connection_queue.append((channel, True)) + self._try_next_connection_from_queue() + + def _start_automesh(self): + """Start meshing automatically, intended when there are no better + networks to connect to. First looks for XS on all channels, then falls + back to simple mesh on channel 1.""" + self._empty_connection_queue() + self._connection_queue.append((1, False)) + self._connection_queue.append((11, True)) + self._connection_queue.append((6, True)) + self._connection_queue.append((1, True)) + self._try_next_connection_from_queue() diff --git a/src/jarabe/model/screen.py b/src/jarabe/model/screen.py new file mode 100644 index 0000000..7d34d45 --- /dev/null +++ b/src/jarabe/model/screen.py @@ -0,0 +1,45 @@ +# Copyright (C) 2006-2008 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus + + +_HARDWARE_MANAGER_INTERFACE = 'org.freedesktop.ohm.Keystore' +_HARDWARE_MANAGER_SERVICE = 'org.freedesktop.ohm' +_HARDWARE_MANAGER_OBJECT_PATH = '/org/freedesktop/ohm/Keystore' + +_ohm_service = None + + +def _get_ohm(): + global _ohm_service + if _ohm_service is None: + bus = dbus.SystemBus() + proxy = bus.get_object(_HARDWARE_MANAGER_SERVICE, + _HARDWARE_MANAGER_OBJECT_PATH, + follow_name_owner_changes=True) + _ohm_service = dbus.Interface(proxy, _HARDWARE_MANAGER_INTERFACE) + + return _ohm_service + + +def set_dcon_freeze(frozen): + try: + _get_ohm().SetKey('display.dcon_freeze', frozen) + except dbus.DBusException: + logging.error('Cannot unfreeze the DCON') diff --git a/src/jarabe/model/session.py b/src/jarabe/model/session.py new file mode 100644 index 0000000..4e66bdc --- /dev/null +++ b/src/jarabe/model/session.py @@ -0,0 +1,113 @@ +# Copyright (C) 2008, Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +import dbus +import os +import signal +import sys +import logging + +from sugar import session +from sugar import env + + +_session_manager = None + + +def have_systemd(): + return os.access("/sys/fs/cgroup/systemd", 0) >= 0 + + +class SessionManager(session.SessionManager): + MODE_LOGOUT = 0 + MODE_SHUTDOWN = 1 + MODE_REBOOT = 2 + + def __init__(self): + session.SessionManager.__init__(self) + self._logout_mode = None + + def logout(self): + self._logout_mode = self.MODE_LOGOUT + self.initiate_shutdown() + + def shutdown(self): + self._logout_mode = self.MODE_SHUTDOWN + self.initiate_shutdown() + + def reboot(self): + self._logout_mode = self.MODE_REBOOT + self.initiate_shutdown() + + def shutdown_completed(self): + if env.is_emulator(): + self._close_emulator() + elif self._logout_mode != self.MODE_LOGOUT: + bus = dbus.SystemBus() + if have_systemd(): + try: + proxy = bus.get_object('org.freedesktop.login1', + '/org/freedesktop/login1') + pm = dbus.Interface(proxy, + 'org.freedesktop.login1.Manager') + + if self._logout_mode == self.MODE_SHUTDOWN: + pm.PowerOff(False) + elif self._logout_mode == self.MODE_REBOOT: + pm.Reboot(True) + except: + logging.exception('Can not stop sugar') + self.session.cancel_shutdown() + return + else: + CONSOLEKIT_DBUS_PATH = '/org/freedesktop/ConsoleKit/Manager' + try: + proxy = bus.get_object('org.freedesktop.ConsoleKit', + CONSOLEKIT_DBUS_PATH) + pm = dbus.Interface(proxy, + 'org.freedesktop.ConsoleKit.Manager') + + if self._logout_mode == self.MODE_SHUTDOWN: + pm.Stop() + elif self._logout_mode == self.MODE_REBOOT: + pm.Restart() + except: + logging.exception('Can not stop sugar') + self.session.cancel_shutdown() + return + + session.SessionManager.shutdown_completed(self) + gtk.main_quit() + + def _close_emulator(self): + gtk.main_quit() + + if 'SUGAR_EMULATOR_PID' in os.environ: + pid = int(os.environ['SUGAR_EMULATOR_PID']) + os.kill(pid, signal.SIGTERM) + + # Need to call this ASAP so the atexit handlers get called before we + # get killed by the X (dis)connection + sys.exit() + + +def get_session_manager(): + global _session_manager + + if _session_manager == None: + _session_manager = SessionManager() + return _session_manager diff --git a/src/jarabe/model/shell.py b/src/jarabe/model/shell.py new file mode 100644 index 0000000..31605f7 --- /dev/null +++ b/src/jarabe/model/shell.py @@ -0,0 +1,675 @@ +# Copyright (C) 2006-2007 Owen Williams. +# Copyright (C) 2006-2008 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import time + +import gconf +import wnck +import gobject +import gtk +import dbus + +from sugar import wm +from sugar import dispatch +from sugar.graphics.xocolor import XoColor + +from jarabe.model.bundleregistry import get_registry + +_SERVICE_NAME = 'org.laptop.Activity' +_SERVICE_PATH = '/org/laptop/Activity' +_SERVICE_INTERFACE = 'org.laptop.Activity' + +_model = None + + +class Activity(gobject.GObject): + """Activity which appears in the "Home View" of the Sugar shell + + This class stores the Sugar Shell's metadata regarding a + given activity/application in the system. It interacts with + the sugar.activity.* modules extensively in order to + accomplish its tasks. + """ + + __gtype_name__ = 'SugarHomeActivity' + + LAUNCHING = 0 + LAUNCH_FAILED = 1 + LAUNCHED = 2 + + def __init__(self, activity_info, activity_id, color, window=None): + """Initialise the HomeActivity + + activity_info -- sugar.activity.registry.ActivityInfo instance, + provides the information required to actually + create the new instance. This is, in effect, + the "type" of activity being created. + activity_id -- unique identifier for this instance + of the activity type + _windows -- WnckWindows registered for the activity. The lowest + one in the stack is the main window. + """ + gobject.GObject.__init__(self) + + self._windows = [] + self._service = None + self._activity_id = activity_id + self._activity_info = activity_info + self._launch_time = time.time() + self._launch_status = Activity.LAUNCHING + + if color is not None: + self._color = color + else: + client = gconf.client_get_default() + color = client.get_string('/desktop/sugar/user/color') + self._color = XoColor(color) + + if window is not None: + self.add_window(window) + + self._retrieve_service() + + self._name_owner_changed_handler = None + if not self._service: + bus = dbus.SessionBus() + self._name_owner_changed_handler = bus.add_signal_receiver( + self._name_owner_changed_cb, + signal_name='NameOwnerChanged', + dbus_interface='org.freedesktop.DBus') + + self._launch_completed_hid = get_model().connect('launch-completed', + self.__launch_completed_cb) + self._launch_failed_hid = get_model().connect('launch-failed', + self.__launch_failed_cb) + + def get_launch_status(self): + return self._launch_status + + launch_status = gobject.property(getter=get_launch_status) + + def add_window(self, window): + """Add a window to the windows stack.""" + if not window: + raise ValueError('window must be valid') + self._windows.append(window) + + def remove_window_by_xid(self, xid): + """Remove a window from the windows stack.""" + for wnd in self._windows: + if wnd.get_xid() == xid: + self._windows.remove(wnd) + return True + return False + + def get_service(self): + """Get the activity service + + Note that non-native Sugar applications will not have + such a service, so the return value will be None in + those cases. + """ + + return self._service + + def get_title(self): + """Retrieve the application's root window's suggested title""" + if self._windows: + return self._windows[0].get_name() + else: + return '' + + def get_icon_path(self): + """Retrieve the activity's icon (file) name""" + if self.is_journal(): + icon_theme = gtk.icon_theme_get_default() + info = icon_theme.lookup_icon('activity-journal', + gtk.ICON_SIZE_SMALL_TOOLBAR, 0) + if not info: + return None + fname = info.get_filename() + del info + return fname + elif self._activity_info: + return self._activity_info.get_icon() + else: + return None + + def get_icon_color(self): + """Retrieve the appropriate icon colour for this activity + + Uses activity_id to index into the PresenceService's + set of activity colours, if the PresenceService does not + have an entry (implying that this is not a Sugar-shared application) + uses the local user's profile colour for the icon. + """ + return self._color + + def get_activity_id(self): + """Retrieve the "activity_id" passed in to our constructor + + This is a "globally likely unique" identifier generated by + sugar.util.unique_id + """ + return self._activity_id + + def get_xid(self): + """Retrieve the X-windows ID of our root window""" + if self._windows: + return self._windows[0].get_xid() + else: + return None + + def has_xid(self, xid): + """Check if an X-window with the given xid is in the windows stack""" + if self._windows: + for wnd in self._windows: + if wnd.get_xid() == xid: + return True + return False + + def get_window(self): + """Retrieve the X-windows root window of this application + + This was stored by the add_window method, which was + called by HomeModel._add_activity, which was called + via a callback that looks for all 'window-opened' + events. + + We keep a stack of the windows. The lowest window in the + stack that is still valid we consider the main one. + + HomeModel currently uses a dbus service query on the + activity to determine to which HomeActivity the newly + launched window belongs. + """ + if self._windows: + return self._windows[0] + return None + + def get_type(self): + """Retrieve the activity bundle id for future reference""" + if not self._windows: + return None + else: + return wm.get_bundle_id(self._windows[0]) + + def is_journal(self): + """Returns boolean if the activity is of type JournalActivity""" + return self.get_type() == 'org.laptop.JournalActivity' + + def get_launch_time(self): + """Return the time at which the activity was first launched + + Format is floating-point time.time() value + (seconds since the epoch) + """ + return self._launch_time + + def get_pid(self): + """Returns the activity's PID""" + if not self._windows: + return None + return self._windows[0].get_pid() + + def get_bundle_path(self): + """Returns the activity's bundle directory""" + if self._activity_info is None: + return None + else: + return self._activity_info.get_path() + + def get_activity_name(self): + """Returns the activity's bundle name""" + if self._activity_info is None: + return None + else: + return self._activity_info.get_name() + + def equals(self, activity): + if self._activity_id and activity.get_activity_id(): + return self._activity_id == activity.get_activity_id() + if self._windows[0].get_xid() and activity.get_xid(): + return self._windows[0].get_xid() == activity.get_xid() + return False + + def _get_service_name(self): + if self._activity_id: + return _SERVICE_NAME + self._activity_id + else: + return None + + def _retrieve_service(self): + if not self._activity_id: + return + + try: + bus = dbus.SessionBus() + proxy = bus.get_object(self._get_service_name(), + _SERVICE_PATH + '/' + self._activity_id) + self._service = dbus.Interface(proxy, _SERVICE_INTERFACE) + except dbus.DBusException: + self._service = None + + def _name_owner_changed_cb(self, name, old, new): + if name == self._get_service_name(): + if old and not new: + logging.debug('Activity._name_owner_changed_cb: ' \ + 'activity %s went away', name) + self._name_owner_changed_handler.remove() + self._name_owner_changed_handler = None + self._service = None + elif not old and new: + logging.debug('Activity._name_owner_changed_cb: ' \ + 'activity %s started up', name) + self._retrieve_service() + self.set_active(True) + + def set_active(self, state): + """Propagate the current state to the activity object""" + if self._service is not None: + self._service.SetActive(state, + reply_handler=self._set_active_success, + error_handler=self._set_active_error) + + def _set_active_success(self): + pass + + def _set_active_error(self, err): + logging.error('set_active() failed: %s', err) + + def _set_launch_status(self, value): + get_model().disconnect(self._launch_completed_hid) + get_model().disconnect(self._launch_failed_hid) + self._launch_completed_hid = None + self._launch_failed_hid = None + self._launch_status = value + self.notify('launch_status') + + def __launch_completed_cb(self, model, home_activity): + if home_activity is self: + self._set_launch_status(Activity.LAUNCHED) + + def __launch_failed_cb(self, model, home_activity): + if home_activity is self: + self._set_launch_status(Activity.LAUNCH_FAILED) + + +class ShellModel(gobject.GObject): + """Model of the shell (activity management) + + The ShellModel is basically the point of registration + for all running activities within Sugar. It traps + events that tell the system there is a new activity + being created (generated by the activity factories), + or removed, as well as those which tell us that the + currently focussed activity has changed. + + The HomeModel tracks a set of HomeActivity instances, + which are tracking the window to activity mappings + the activity factories have set up. + """ + + __gsignals__ = { + 'activity-added': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'activity-removed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'active-activity-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'tabbing-activity-changed': (gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'launch-started': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'launch-completed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + 'launch-failed': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, + ([gobject.TYPE_PYOBJECT])), + } + + ZOOM_MESH = 0 + ZOOM_GROUP = 1 + ZOOM_HOME = 2 + ZOOM_ACTIVITY = 3 + + def __init__(self): + gobject.GObject.__init__(self) + + self._screen = wnck.screen_get_default() + self._screen.connect('window-opened', self._window_opened_cb) + self._screen.connect('window-closed', self._window_closed_cb) + self._screen.connect('active-window-changed', + self._active_window_changed_cb) + + self.zoom_level_changed = dispatch.Signal() + + self._desktop_level = self.ZOOM_HOME + self._zoom_level = self.ZOOM_HOME + self._current_activity = None + self._activities = [] + self._shared_activities = {} + self._active_activity = None + self._tabbing_activity = None + self._launchers = {} + + self._screen.toggle_showing_desktop(True) + + def get_launcher(self, activity_id): + return self._launchers.get(str(activity_id)) + + def register_launcher(self, activity_id, launcher): + self._launchers[activity_id] = launcher + + def unregister_launcher(self, activity_id): + if activity_id in self._launchers: + del self._launchers[activity_id] + + def _update_zoom_level(self, window): + if window.get_window_type() == wnck.WINDOW_DIALOG: + return + elif window.get_window_type() == wnck.WINDOW_NORMAL: + new_level = self.ZOOM_ACTIVITY + else: + new_level = self._desktop_level + + if self._zoom_level != new_level: + old_level = self._zoom_level + self._zoom_level = new_level + self.zoom_level_changed.send(self, old_level=old_level, + new_level=new_level) + + def set_zoom_level(self, new_level, x_event_time=0): + old_level = self.zoom_level + if old_level == new_level: + return + + if old_level != self.ZOOM_ACTIVITY: + screen = gtk.gdk.screen_get_default() + active_window_type = screen.get_active_window().get_type_hint() + if active_window_type != gtk.gdk.WINDOW_TYPE_HINT_DESKTOP: + return + + self._zoom_level = new_level + if new_level is not self.ZOOM_ACTIVITY: + self._desktop_level = new_level + + self.zoom_level_changed.send(self, old_level=old_level, + new_level=new_level) + + show_desktop = new_level is not self.ZOOM_ACTIVITY + self._screen.toggle_showing_desktop(show_desktop) + + if new_level is self.ZOOM_ACTIVITY: + # activate the window, in case it was iconified + # (e.g. during sugar launch, the Journal starts in this state) + window = self._active_activity.get_window() + if window: + window.activate(x_event_time or gtk.get_current_event_time()) + + def _get_zoom_level(self): + return self._zoom_level + + zoom_level = property(_get_zoom_level) + + def _get_activities_with_window(self): + ret = [] + for i in self._activities: + if i.get_window() is not None: + ret.append(i) + return ret + + def get_previous_activity(self, current=None): + if not current: + current = self._active_activity + + activities = self._get_activities_with_window() + i = activities.index(current) + if len(activities) == 0: + return None + elif i - 1 >= 0: + return activities[i - 1] + else: + return activities[len(activities) - 1] + + def get_next_activity(self, current=None): + if not current: + current = self._active_activity + + activities = self._get_activities_with_window() + i = activities.index(current) + if len(activities) == 0: + return None + elif i + 1 < len(activities): + return activities[i + 1] + else: + return activities[0] + + def get_active_activity(self): + """Returns the activity that the user is currently working in""" + return self._active_activity + + def add_shared_activity(self, activity_id, color): + self._shared_activities[activity_id] = color + + def remove_shared_activity(self, activity_id): + del self._shared_activities[activity_id] + + def get_tabbing_activity(self): + """Returns the activity that is currently highlighted during tabbing""" + return self._tabbing_activity + + def set_tabbing_activity(self, activity): + """Sets the activity that is currently highlighted during tabbing""" + self._tabbing_activity = activity + self.emit('tabbing-activity-changed', self._tabbing_activity) + + def _set_active_activity(self, home_activity): + if self._active_activity == home_activity: + return + + if home_activity: + home_activity.set_active(True) + + if self._active_activity: + self._active_activity.set_active(False) + + self._active_activity = home_activity + self.emit('active-activity-changed', self._active_activity) + + def __iter__(self): + return iter(self._activities) + + def __len__(self): + return len(self._activities) + + def __getitem__(self, i): + return self._activities[i] + + def index(self, obj): + return self._activities.index(obj) + + def _window_opened_cb(self, screen, window): + """Handle the callback for the 'window opened' event. + + Most activities will register 2 windows during + their lifetime: the launcher window, and the 'main' + app window. + + When the main window appears, we send a signal to + the launcher window to close. + + Some activities (notably non-native apps) open several + windows during their lifetime, switching from one to + the next as the 'main' window. We use a stack to track + them. + + """ + if window.get_window_type() == wnck.WINDOW_NORMAL: + home_activity = None + + activity_id = wm.get_activity_id(window) + + service_name = wm.get_bundle_id(window) + if service_name: + registry = get_registry() + activity_info = registry.get_bundle(service_name) + else: + activity_info = None + + if activity_id: + home_activity = self.get_activity_by_id(activity_id) + + xid = window.get_xid() + gdk_window = gtk.gdk.window_foreign_new(xid) + gdk_window.set_decorations(0) + + window.maximize() + + if not home_activity: + logging.debug('first window registered for %s', activity_id) + color = self._shared_activities.get(activity_id, None) + home_activity = Activity(activity_info, activity_id, + color, window) + self._add_activity(home_activity) + else: + logging.debug('window registered for %s', activity_id) + home_activity.add_window(window) + + if wm.get_sugar_window_type(window) != 'launcher' \ + and home_activity.get_launch_status() == Activity.LAUNCHING: + self.emit('launch-completed', home_activity) + startup_time = time.time() - home_activity.get_launch_time() + logging.debug('%s launched in %f seconds.', + activity_id, startup_time) + + if self._active_activity is None: + self._set_active_activity(home_activity) + + def _window_closed_cb(self, screen, window): + if window.get_window_type() == wnck.WINDOW_NORMAL: + xid = window.get_xid() + activity = self._get_activity_by_xid(xid) + if activity is not None: + activity.remove_window_by_xid(xid) + if activity.get_window() is None: + logging.debug('last window gone - remove activity %s', + activity) + self._remove_activity(activity) + + def _get_activity_by_xid(self, xid): + for home_activity in self._activities: + if home_activity.has_xid(xid): + return home_activity + return None + + def get_activity_by_id(self, activity_id): + for home_activity in self._activities: + if home_activity.get_activity_id() == activity_id: + return home_activity + return None + + def _active_window_changed_cb(self, screen, previous_window=None): + window = screen.get_active_window() + if window is None: + return + + if window.get_window_type() != wnck.WINDOW_DIALOG: + while window.get_transient() is not None: + window = window.get_transient() + + act = self._get_activity_by_xid(window.get_xid()) + if act is not None: + self._set_active_activity(act) + + self._update_zoom_level(window) + + def _add_activity(self, home_activity): + self._activities.append(home_activity) + self.emit('activity-added', home_activity) + + def _remove_activity(self, home_activity): + if home_activity == self._active_activity: + windows = wnck.screen_get_default().get_windows_stacked() + windows.reverse() + for window in windows: + new_activity = self._get_activity_by_xid(window.get_xid()) + if new_activity is not None: + self._set_active_activity(new_activity) + break + else: + logging.error('No activities are running') + self._set_active_activity(None) + + self.emit('activity-removed', home_activity) + self._activities.remove(home_activity) + + def notify_launch(self, activity_id, service_name): + registry = get_registry() + activity_info = registry.get_bundle(service_name) + if not activity_info: + raise ValueError("Activity service name '%s'" \ + " was not found in the bundle registry." + % service_name) + color = self._shared_activities.get(activity_id, None) + home_activity = Activity(activity_info, activity_id, color) + self._add_activity(home_activity) + + self._set_active_activity(home_activity) + + self.emit('launch-started', home_activity) + + # FIXME: better learn about finishing processes by receiving a signal. + # Now just check whether an activity has a window after ~90sec + gobject.timeout_add_seconds(90, self._check_activity_launched, + activity_id) + + def notify_launch_failed(self, activity_id): + home_activity = self.get_activity_by_id(activity_id) + if home_activity: + logging.debug('Activity %s (%s) launch failed', activity_id, + home_activity.get_type()) + if self.get_launcher(activity_id) is not None: + self.emit('launch-failed', home_activity) + else: + # activity sent failure notification after closing launcher + self._remove_activity(home_activity) + else: + logging.error('Model for activity id %s does not exist.', + activity_id) + + def _check_activity_launched(self, activity_id): + home_activity = self.get_activity_by_id(activity_id) + + if not home_activity: + logging.debug('Activity %s has been closed already.', activity_id) + return False + + if self.get_launcher(activity_id) is not None: + logging.debug('Activity %s still launching, assuming it failed.', + activity_id) + self.notify_launch_failed(activity_id) + return False + + +def get_model(): + global _model + if _model is None: + _model = ShellModel() + return _model diff --git a/src/jarabe/model/sound.py b/src/jarabe/model/sound.py new file mode 100644 index 0000000..9e1e748 --- /dev/null +++ b/src/jarabe/model/sound.py @@ -0,0 +1,65 @@ +# Copyright (C) 2006-2008 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gconf + +from sugar import env +from sugar import _sugarext +from sugar import dispatch + + +VOLUME_STEP = 10 + +muted_changed = dispatch.Signal() +volume_changed = dispatch.Signal() + +_volume = _sugarext.VolumeAlsa() + + +def get_muted(): + return _volume.get_mute() + + +def get_volume(): + return _volume.get_volume() + + +def set_volume(new_volume): + old_volume = _volume.get_volume() + _volume.set_volume(new_volume) + + volume_changed.send(None) + save() + + +def set_muted(new_state): + old_state = _volume.get_mute() + _volume.set_mute(new_state) + + muted_changed.send(None) + save() + + +def save(): + if env.is_emulator() is False: + client = gconf.client_get_default() + client.set_int('/desktop/sugar/sound/volume', get_volume()) + + +def restore(): + if env.is_emulator() is False: + client = gconf.client_get_default() + set_volume(client.get_int('/desktop/sugar/sound/volume')) diff --git a/src/jarabe/model/speech.py b/src/jarabe/model/speech.py new file mode 100644 index 0000000..1cb0ad4 --- /dev/null +++ b/src/jarabe/model/speech.py @@ -0,0 +1,232 @@ +# Copyright (C) 2011 One Laptop Per Child +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging + +import gconf +import gst +import gtk +import gobject + + +DEFAULT_PITCH = 0 + + +DEFAULT_RATE = 0 + +_speech_manager = None + + +class SpeechManager(gobject.GObject): + + __gtype_name__ = 'SpeechManager' + + __gsignals__ = { + 'play': (gobject.SIGNAL_RUN_FIRST, None, []), + 'pause': (gobject.SIGNAL_RUN_FIRST, None, []), + 'stop': (gobject.SIGNAL_RUN_FIRST, None, []) + } + + MIN_PITCH = -100 + MAX_PITCH = 100 + + MIN_RATE = -100 + MAX_RATE = 100 + + def __init__(self, **kwargs): + gobject.GObject.__init__(self, **kwargs) + self._player = _GstSpeechPlayer() + self._player.connect('play', self._update_state, 'play') + self._player.connect('stop', self._update_state, 'stop') + self._player.connect('pause', self._update_state, 'pause') + self._voice_name = self._player.get_default_voice() + self._pitch = DEFAULT_PITCH + self._rate = DEFAULT_RATE + self._is_playing = False + self._is_paused = False + self.restore() + + def _update_state(self, player, signal): + self._is_playing = (signal == 'play') + self._is_paused = (signal == 'pause') + self.emit(signal) + + def get_is_playing(self): + return self._is_playing + + is_playing = gobject.property(type=bool, getter=get_is_playing, + setter=None, default=False) + + def get_is_paused(self): + return self._is_paused + + is_paused = gobject.property(type=bool, getter=get_is_paused, + setter=None, default=False) + + def get_pitch(self): + return self._pitch + + def get_rate(self): + return self._rate + + def set_pitch(self, pitch): + self._pitch = pitch + self.save() + + def set_rate(self, rate): + self._rate = rate + self.save() + + def say_text(self, text): + if text: + self._player.speak(self._pitch, self._rate, self._voice_name, text) + + def say_selected_text(self): + clipboard = gtk.clipboard_get(selection='PRIMARY') + clipboard.request_text(self.__primary_selection_cb) + + def pause(self): + self._player.pause_sound_device() + + def restart(self): + self._player.restart_sound_device() + + def stop(self): + self._player.stop_sound_device() + + def __primary_selection_cb(self, clipboard, text, user_data): + self.say_text(text) + + def save(self): + client = gconf.client_get_default() + client.set_int('/desktop/sugar/speech/pitch', self._pitch) + client.set_int('/desktop/sugar/speech/rate', self._rate) + logging.debug('saving speech configuration pitch %s rate %s', + self._pitch, self._rate) + + def restore(self): + client = gconf.client_get_default() + self._pitch = client.get_int('/desktop/sugar/speech/pitch') + self._rate = client.get_int('/desktop/sugar/speech/rate') + logging.debug('loading speech configuration pitch %s rate %s', + self._pitch, self._rate) + + +class _GstSpeechPlayer(gobject.GObject): + + __gsignals__ = { + 'play': (gobject.SIGNAL_RUN_FIRST, None, []), + 'pause': (gobject.SIGNAL_RUN_FIRST, None, []), + 'stop': (gobject.SIGNAL_RUN_FIRST, None, []) + } + + def __init__(self): + gobject.GObject.__init__(self) + self._pipeline = None + + def restart_sound_device(self): + if self._pipeline is None: + logging.debug('Trying to restart not initialized sound device') + return + + self._pipeline.set_state(gst.STATE_PLAYING) + self.emit('play') + + def pause_sound_device(self): + if self._pipeline is None: + return + + self._pipeline.set_state(gst.STATE_PAUSED) + self.emit('pause') + + def stop_sound_device(self): + if self._pipeline is None: + return + + self._pipeline.set_state(gst.STATE_NULL) + self.emit('stop') + + def make_pipeline(self, command): + if self._pipeline is not None: + self.stop_sound_device() + del self._pipeline + + self._pipeline = gst.parse_launch(command) + + bus = self._pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self.__pipe_message_cb) + + def __pipe_message_cb(self, bus, message): + if message.type == gst.MESSAGE_EOS: + self._pipeline.set_state(gst.STATE_NULL) + self.emit('stop') + elif message.type == gst.MESSAGE_ERROR: + self._pipeline.set_state(gst.STATE_NULL) + self.emit('stop') + + def speak(self, pitch, rate, voice_name, text): + # TODO workaround for http://bugs.sugarlabs.org/ticket/1801 + if not [i for i in text if i.isalnum()]: + return + + self.make_pipeline('espeak name=espeak ! autoaudiosink') + src = self._pipeline.get_by_name('espeak') + + src.props.text = text + src.props.pitch = pitch + src.props.rate = rate + src.props.voice = voice_name + src.props.track = 2 # track for marks + + self.restart_sound_device() + + def get_all_voices(self): + all_voices = {} + for voice in gst.element_factory_make('espeak').props.voices: + name, language, dialect = voice + if dialect != 'none': + all_voices[language + '_' + dialect] = name + else: + all_voices[language] = name + return all_voices + + def get_default_voice(self): + """Try to figure out the default voice, from the current locale ($LANG) + Fall back to espeak's voice called Default.""" + voices = self.get_all_voices() + + locale = os.environ.get('LANG', '') + language_location = locale.split('.', 1)[0].lower() + language = language_location.split('_')[0] + # if the language is es but not es_es default to es_la (latin voice) + if language == 'es' and language_location != 'es_es': + language_location = 'es_la' + + best = voices.get(language_location) or voices.get(language) \ + or 'default' + logging.debug('Best voice for LANG %s seems to be %s', + locale, best) + return best + + +def get_speech_manager(): + global _speech_manager + + if _speech_manager is None: + _speech_manager = SpeechManager() + return _speech_manager diff --git a/src/jarabe/model/telepathyclient.py b/src/jarabe/model/telepathyclient.py new file mode 100644 index 0000000..2604af6 --- /dev/null +++ b/src/jarabe/model/telepathyclient.py @@ -0,0 +1,126 @@ +# Copyright (C) 2010 Collabora Ltd. <http://www.collabora.co.uk/> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging + +import dbus +from dbus import PROPERTIES_IFACE +from telepathy.interfaces import CLIENT, \ + CHANNEL, \ + CHANNEL_TYPE_TEXT, \ + CLIENT_APPROVER, \ + CLIENT_HANDLER, \ + CLIENT_INTERFACE_REQUESTS +from telepathy.server import DBusProperties + +from telepathy.constants import CONNECTION_HANDLE_TYPE_ROOM +from telepathy.constants import CONNECTION_HANDLE_TYPE_CONTACT + +from sugar import dispatch + + +SUGAR_CLIENT_SERVICE = 'org.freedesktop.Telepathy.Client.Sugar' +SUGAR_CLIENT_PATH = '/org/freedesktop/Telepathy/Client/Sugar' + +_instance = None + + +class TelepathyClient(dbus.service.Object, DBusProperties): + def __init__(self): + self._interfaces = set([CLIENT, CLIENT_HANDLER, + CLIENT_INTERFACE_REQUESTS, PROPERTIES_IFACE, + CLIENT_APPROVER]) + + bus = dbus.Bus() + bus_name = dbus.service.BusName(SUGAR_CLIENT_SERVICE, bus=bus) + + dbus.service.Object.__init__(self, bus_name, SUGAR_CLIENT_PATH) + DBusProperties.__init__(self) + + self._implement_property_get(CLIENT, { + 'Interfaces': lambda: list(self._interfaces), + }) + self._implement_property_get(CLIENT_HANDLER, { + 'HandlerChannelFilter': self.__get_filters_handler_cb, + }) + self._implement_property_get(CLIENT_APPROVER, { + 'ApproverChannelFilter': self.__get_filters_approver_cb, + }) + + self.got_channel = dispatch.Signal() + self.got_dispatch_operation = dispatch.Signal() + + def __get_filters_handler_cb(self): + filter_dict = dbus.Dictionary({}, signature='sv') + return dbus.Array([filter_dict], signature='a{sv}') + + def __get_filters_approver_cb(self): + activity_invitation = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_TEXT, + CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_ROOM, + } + filter_dict = dbus.Dictionary(activity_invitation, signature='sv') + filters = dbus.Array([filter_dict], signature='a{sv}') + + text_invitation = { + CHANNEL + '.ChannelType': CHANNEL_TYPE_TEXT, + CHANNEL + '.TargetHandleType': CONNECTION_HANDLE_TYPE_CONTACT, + } + filter_dict = dbus.Dictionary(text_invitation, signature='sv') + filters.append(filter_dict) + + logging.debug('__get_filters_approver_cb %r', filters) + + return filters + + @dbus.service.method(dbus_interface=CLIENT_HANDLER, + in_signature='ooa(oa{sv})aota{sv}', out_signature='') + def HandleChannels(self, account, connection, channels, requests_satisfied, + user_action_time, handler_info): + logging.debug('HandleChannels\n%r\n%r\n%r\n%r\n%r\n%r\n', account, + connection, channels, requests_satisfied, + user_action_time, handler_info) + for channel in channels: + self.got_channel.send(self, account=account, + connection=connection, channel=channel) + + @dbus.service.method(dbus_interface=CLIENT_INTERFACE_REQUESTS, + in_signature='oa{sv}', out_signature='') + def AddRequest(self, request, properties): + logging.debug('AddRequest\n%r\n%r', request, properties) + + @dbus.service.method(dbus_interface=CLIENT_APPROVER, + in_signature='a(oa{sv})oa{sv}', out_signature='', + async_callbacks=('success_cb', 'error_cb_')) + def AddDispatchOperation(self, channels, dispatch_operation_path, + properties, success_cb, error_cb_): + success_cb() + try: + logging.debug('AddDispatchOperation\n%r\n%r\n%r', channels, + dispatch_operation_path, properties) + + self.got_dispatch_operation.send(self, channels=channels, + dispatch_operation_path=dispatch_operation_path, + properties=properties) + except Exception, e: + logging.exception(e) + + +def get_instance(): + global _instance + if not _instance: + _instance = TelepathyClient() + return _instance |