Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/jarabe/model
diff options
context:
space:
mode:
Diffstat (limited to 'src/jarabe/model')
-rw-r--r--src/jarabe/model/Makefile.am20
-rw-r--r--src/jarabe/model/Makefile.in457
-rw-r--r--src/jarabe/model/__init__.py15
-rw-r--r--src/jarabe/model/adhoc.py282
-rw-r--r--src/jarabe/model/buddy.py213
-rw-r--r--src/jarabe/model/bundleregistry.py450
-rw-r--r--src/jarabe/model/filetransfer.py368
-rw-r--r--src/jarabe/model/friends.py174
-rw-r--r--src/jarabe/model/invites.py289
-rw-r--r--src/jarabe/model/mimeregistry.py50
-rw-r--r--src/jarabe/model/neighborhood.py1084
-rw-r--r--src/jarabe/model/network.py1096
-rw-r--r--src/jarabe/model/notifications.py98
-rw-r--r--src/jarabe/model/olpcmesh.py228
-rw-r--r--src/jarabe/model/screen.py45
-rw-r--r--src/jarabe/model/session.py113
-rw-r--r--src/jarabe/model/shell.py675
-rw-r--r--src/jarabe/model/sound.py65
-rw-r--r--src/jarabe/model/speech.py232
-rw-r--r--src/jarabe/model/telepathyclient.py126
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