# Copyright (C) 2012 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 3 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, see . import shutil import logging from gettext import gettext as _ from gi.repository import Gtk from gi.repository import GObject from pylru import lrucache from sugar3 import util from sugar_network import clones from sugar3.bundle.activitybundle import ActivityBundle from sugar3.bundle.contentbundle import ContentBundle from sugar3.bundle.bundleversion import NormalizedVersion from sugar3.bundle.bundle import AlreadyInstalledException from jarabe.model import mimeregistry from jarabe.plugins.sn import SN_BROWSER_NAME, get_client from jarabe.journal.journalentrybundle import JournalEntryBundle _logger = logging.getLogger('plugins.sn.bundleregistry') _stub_icon_path = None _online_cache = lrucache(32) def stub_icon(): global _stub_icon_path if not _stub_icon_path: theme = Gtk.IconTheme.get_default() _stub_icon_path = theme.lookup_icon('empty', 0, 0).get_filename() return _stub_icon_path 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): GObject.GObject.__init__(self) self._bundles = {} get_client().connect('event', self.__Event_cb) self._populate() def __iter__(self): return self._bundles.itervalues() def __len__(self): return len(self._bundles) def get_bundle(self, context_guid, mountpoint='~'): if context_guid == 'org.laptop.JournalActivity': return None elif context_guid == SN_BROWSER_NAME: return _BrowserInfo() if context_guid in self._bundles: return self._bundles[context_guid] if mountpoint == '~': if context_guid in _online_cache: return _online_cache[context_guid] return None try: props = get_client().get(['context', context_guid], mountpoint=mountpoint, reply=['guid', 'keep', 'keep_impl', 'title']) bundle = _ContextInfo(mountpoint, props) except Exception: _logger.warning('Cannot fetch activity metadata for %r on %r', context_guid, mountpoint) bundle = None # Keep even None budles to avoid needless retrying _online_cache[context_guid] = bundle return bundle 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.values(): if mime_type in (bundle.get_mime_types() or []): if bundle.get_bundle_id() == default_bundle_id: default_bundle = bundle else: result.append(bundle) if default_bundle is not None: result.insert(0, default_bundle) return result def is_bundle_favorite(self, bundle_id, version): bundle = self._bundles.get(bundle_id) if bundle is None: return False return bundle.props['keep'] def set_bundle_favorite(self, bundle_id, version, keep): bundle = self._bundles.get(bundle_id) if bundle is None: return get_client().put(['context', bundle_id], {'keep': keep}, mountpoint='~') bundle.props['keep'] = keep self.emit('bundle-changed', bundle) def get_bundle_position(self, bundle_id, version): bundle = self._bundles.get(bundle_id) if bundle is None: return -1, -1 return bundle.props['position'] def set_bundle_position(self, bundle_id, version, x, y): bundle = self._bundles.get(bundle_id) if bundle is None: return position = bundle.props['position'] = [int(x), int(y)] get_client().put(['context', bundle_id], {'position': position}, mountpoint='~') self.emit('bundle-changed', bundle) def is_activity_protected(self, bundle_id): # No need, is_user_activity() is enough return False def is_installed(self, bundle): if isinstance(bundle, ContentBundle) or \ isinstance(bundle, JournalEntryBundle): return bundle.is_installed() installed = self._bundles.get(bundle.get_bundle_id()) return installed is not None and \ NormalizedVersion(bundle.get_activity_version()) == \ NormalizedVersion(installed.get_activity_version()) def install(self, bundle, uid=None, force_downgrade=False): if isinstance(bundle, JournalEntryBundle): bundle.install(uid) return elif isinstance(bundle, ContentBundle): bundle.install() return installed = self._bundles.get(bundle.get_bundle_id()) if installed is not None: if not force_downgrade and \ NormalizedVersion(bundle.get_activity_version()) <= \ NormalizedVersion(installed.get_activity_version()): raise AlreadyInstalledException installed.uninstall() bundle.install() def uninstall(self, bundle, force=False, delete_profile=False): if isinstance(bundle, ContentBundle) or \ isinstance(bundle, JournalEntryBundle): if bundle.is_installed(): bundle.uninstall() else: bundle = self._bundles.get(bundle.get_bundle_id()) if bundle is not None: bundle.uninstall() def upgrade(self, bundle): self.install(bundle) def _populate(self): response = get_client().get(['context'], mountpoint='~', reply=['guid', 'keep', 'keep_impl', 'position'], # TODO process result by portions less than 1024 limit=1024, keep_impl=2) for props in response['result']: if props['guid'] in self._bundles: continue self._add_bundle(props['guid'], props) def _add_bundle(self, bundle_id, props): bundle = None for path in clones(bundle_id): try: bundle = _BundleInfo(ActivityBundle(path)) break except Exception: _logger.exception('Cannot load %r bundle from %r', bundle_id, path) if bundle is None: _logger.info('No bundles for %r', bundle_id) return None bundle.props = props self._bundles[bundle.get_bundle_id()] = bundle _logger.info('Add %r bundle', bundle.get_bundle_id()) self.emit('bundle-added', bundle) def _remove_bundle(self, bundle_id): if bundle_id not in self._bundles: return _logger.info('Remove %r bundle', bundle_id) bundle = self._bundles.pop(bundle_id) self.emit('bundle-removed', bundle) def _set_keep(self, bundle_id, keep): bundle = self._bundles.get(bundle_id) if bundle is None: return bundle.props['keep'] = keep self.emit('bundle-changed', bundle) def _set_keep_impl(self, bundle_id, keep_impl): if keep_impl: props = get_client().get(['context', bundle_id], mountpoint='~', reply=['guid', 'keep', 'keep_impl', 'position']) bundle = self._bundles.get(bundle_id) if bundle is None: bundle = self._add_bundle(bundle_id, props) else: bundle.props = props self.emit('bundle-changed', bundle) else: self._remove_bundle(bundle_id) def __Event_cb(self, event, data): if 'mountpoint' in data and data['mountpoint'] != '~' or \ data.get('document') != 'context': return if event in ('create', 'update'): bundle_id = data['guid'] props = data['props'] if props.get('keep_impl') in (0, 2): self._set_keep_impl(bundle_id, props['keep_impl']) if 'keep' in props: self._set_keep(bundle_id, props['keep']) elif event == 'delete': bundle_id = data['guid'] self._set_keep_impl(bundle_id, 0) elif event == 'populate': self._populate() class _ContextInfo(object): def __init__(self, mountpoint, props): self.mountpoint = mountpoint self.props = props self._tmp_icon = None def get_name(self): return self.props['title'] def get_bundle_id(self): return self.props['guid'] def get_icon(self): if self._tmp_icon is None: blob = None try: blob = get_client().get( ['context', self.get_bundle_id(), 'artifact_icon'], mountpoint=self.mountpoint) except Exception, error: _logger.debug('Fail to get icon for %r: %s', self.get_bundle_id(), error) if not blob: self._tmp_icon = stub_icon() else: self._tmp_icon = util.TempFilePath() with file(self._tmp_icon, 'w') as f: f.write(blob) # Cast to `str to avoid spreading `util.TempFilePath` return str(self._tmp_icon) def get_tags(self): # Doesn't matter with Sweets' features enabled return [] def get_activity_version(self): return '' def get_installation_time(self): return 0 def get_command(self): # Doesn't matter with Sweets' features enabled return '' def is_user_activity(self): return True def get_path(self): return '/' def get_mime_types(self): return [] class _BundleInfo(object): def __init__(self, bundle): self.props = { 'guid': bundle.get_bundle_id, 'keep': False, 'position': (-1, -1), } self.mountpoint = '~' self._bundle = bundle def uninstall(self): _logger.debug('Uninstall %r', self._bundle.get_bundle_id()) shutil.rmtree(self._bundle.get_path(), ignore_errors=True) def __getattr__(self, name): return getattr(self._bundle, name) class _BrowserInfo(object): def __init__(self): icon = Gtk.IconTheme.get_default().lookup_icon('sugar-network', 0, 0) self._icon_path = icon.get_filename() def get_name(self): return _('Sugar Network') def get_bundle_id(self): return SN_BROWSER_NAME def get_icon(self): return self._icon_path def get_tags(self): return [] def get_activity_version(self): return '0' def get_installation_time(self): return 0 def get_command(self): return '' def is_user_activity(self): return False def get_path(self): return '/'