Пример #1
0
    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()
Пример #2
0
def _check_for_updates(url, activity_id):
    """Downloads the given URL, parses it (caching the result), and returns
    a list of (version, url) pairs present for the given `activity_id`.
    Returns `None` if there was a problem downloading the URL.
    Returns a zero-length list if the given URL is unparsable or does not
    contain information for the desired activity_id."""
    global _parse_cache
    if url not in _parse_cache:
        try:
            __, __, _parse_cache[url] = \
                microformat.parse_url(url, timeout=HTTP_TIMEOUT)
            if _DEBUG_CHECK_VERSIONS:
                # for kicks and giggles, verify these version #s!
                for n_activity_id, versions in _parse_cache[url].items():
                    for ver, url2 in versions:
                        actual_id, actual_ver = \
                                   actutils.id_and_version_from_url(url2)
                        if actual_id != n_activity_id:
                            print "ACTIVITY ID SHOULD BE", n_activity_id, \
                                  "BUT ACTUALLY IS", actual_id, ("(%s)"%url2)
                        if NormalizedVersion(actual_ver) != NormalizedVersion(
                                ver):
                            print "VERSION SHOULD BE", ver, \
                                  "BUT ACTUALLY IS", actual_ver, ("(%s)"%url2)
        except HTMLParseError:
            _parse_cache[url] = {}  # parse error
        except (IOError, socket.error):
            _parse_cache[url] = None  # network error
    activity_map = _parse_cache[url]
    if activity_map is None: return None  # error attempting to check.
    if activity_id not in activity_map: return []  # no versions found.
    return activity_map[activity_id]
Пример #3
0
    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
Пример #4
0
    def _merge_default_favorites(self):
        # Only merge defaults to _DEFAULT_VIEW
        default_activities = []
        defaults_path = os.environ["SUGAR_ACTIVITIES_DEFAULTS"]
        if os.path.exists(defaults_path):
            file_mtime = os.stat(defaults_path).st_mtime
            if file_mtime > self._last_defaults_mtime[_DEFAULT_VIEW]:
                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[_DEFAULT_VIEW] = file_mtime

        if not default_activities:
            return

        for bundle_id in default_activities:
            max_version = '0'
            for bundle in self:
                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[_DEFAULT_VIEW]:
                self._favorite_bundles[_DEFAULT_VIEW][key] = None

        logging.debug('After merging: %r',
                      self._favorite_bundles[_DEFAULT_VIEW])

        self._write_favorites_file(_DEFAULT_VIEW)
Пример #5
0
 def is_installed(self, bundle):
     for installed_bundle in self:
         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
Пример #6
0
        def refresh_existing(row):
            """Look for updates to an existing activity."""
            act = row[ACTIVITY_BUNDLE]
            oldver = 0 if _DEBUG_MAKE_ALL_OLD else act.get_activity_version()
            size = 0

            def net_good(url_):
                self._saw_network_success = True

            def net_bad(url):
                self._network_failures.append(url)

            # activity group entries have UPDATE_EXISTS=True
            # for any activities not present in the group, try their update_url
            # (if any) for new updates
            # note the behaviour here: if the XS (which hosts activity groups)
            # has an entry for the activity, then we trust that it is the
            # latest and we don't go online to check.
            # we only go online for activities which the XS does not know about
            # the purpose of this is to reduce the high latency of having
            # to check multiple update_urls on a slow connection.

            if row[UPDATE_EXISTS]:
                # trust what the XS told us
                newver, newurl = row[UPDATE_VERSION], row[UPDATE_URL]
            else:
                # hit the internet for updates
                oldver, newver, newurl, size = \
                    _retrieve_update_version(act, net_good, net_bad)

            # make sure that the version we found is actually newer...
            if newver is not None and NormalizedVersion(newver) <= \
                    NormalizedVersion(act.get_activity_version()):
                newver = None
            elif row[UPDATE_EXISTS]:
                # since we trusted the activity group page above, we don't
                # know the size of this bundle. but if we're about to offer it
                # as an update then we should look that up now, with an HTTP
                # request.
                # (by avoiding a load of HTTP requests on activity versions that
                #  we already have, we greatly increase the speed and usability
                #  of this updater on high-latency connections)
                size = urlrange.urlopen(row[UPDATE_URL], timeout=HTTP_TIMEOUT)\
                       .length()

            row[UPDATE_EXISTS] = (newver is not None)
            row[UPDATE_URL] = newurl
            row[UPDATE_SIZE] = size
            if newver is None:
                description = _('At version %s') % oldver
            else:
                description = \
                    _('From version %(old)s to %(new)s (Size: %(size)s)') % \
                    { 'old':oldver, 'new':newver, 'size':_humanize_size(size) }
                row[UPDATE_SELECTED] = True
            row[DESCRIPTION_SMALL] = description
Пример #7
0
    def add_bundle(self,
                   bundle_path,
                   set_favorite=False,
                   emit_signals=True,
                   force_downgrade=False):
        """
        Add a bundle to the registry.
        If the bundle is a duplicate with one already in the registry,
        the existing one from the registry is returned.
        Otherwise, the newly added bundle is returned on success, or None on
        failure.
        """
        try:
            bundle = bundle_from_dir(bundle_path)
        except MalformedBundleException:
            logging.exception('Error loading bundle %r', bundle_path)
            return None

        # None is a valid return value from bundle_from_dir helper.
        if bundle is None:
            logging.error('No bundle in %r', bundle_path)
            return None

        bundle_id = bundle.get_bundle_id()
        logging.debug('STARTUP: Adding bundle %s', 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("Bundle already known")
                return installed
            if not force_downgrade and \
                    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(), emit_signals)

        if set_favorite:
            favorite = not self.is_bundle_hidden(bundle.get_bundle_id(),
                                                 bundle.get_activity_version())
            self._set_bundle_favorite(bundle.get_bundle_id(),
                                      bundle.get_activity_version(), favorite)

        with self._lock:
            self._bundles.append(bundle)
        if emit_signals:
            self.emit('bundle-added', bundle)
        return bundle
Пример #8
0
    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
Пример #9
0
    def uninstall(self, bundle, force=False, delete_profile=False):
        """
        Uninstall a bundle.

        If a different version of bundle is found in the activity registry,
        this function does nothing unless force is True.

        If the bundle is not found in the activity registry at all,
        this function simply returns.
        """
        act = self.get_bundle(bundle.get_bundle_id())
        if not act:
            logging.debug("Bundle is not installed")
            return

        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(force, delete_profile)
        self.remove_bundle(install_path)

        alt_bundles = self.get_system_bundles(act.get_bundle_id())
        if alt_bundles:
            alt_bundles.sort(
                key=lambda b: NormalizedVersion(b.get_activity_version()))
            alt_bundles.reverse()
            new_bundle = alt_bundles[0]
            self.add_bundle(new_bundle.get_path())
Пример #10
0
    def _process_result(self):
        document = XML(self._xml_data)

        if document.find(_FIND_DESCRIPTION) is None:
            logging.debug(
                'Bundle %s not available in the server for the '
                'version %s', self._bundle.get_bundle_id(), config.version)
            version = None
            link = None
            size = None
        else:
            try:
                version = NormalizedVersion(document.find(_FIND_VERSION).text)
            except InvalidVersionError:
                logging.exception('Exception occured while parsing version')
                version = '0'

            link = document.find(_FIND_LINK).text

            try:
                size = long(document.find(_FIND_SIZE).text) * 1024
            except ValueError:
                logging.exception('Exception occured while parsing size')
                size = 0

        global _fetcher
        _fetcher = None
        self._completion_cb(self._bundle, version, link, size, None)
Пример #11
0
    def _filter_results(self):
        # Remove updates for which we already have an equivalent or newer
        # version installed. Queue the remaining ones to be checked.
        registry = bundleregistry.get_registry()
        self._bundles_to_check = []
        for bundle_id, data in list(self._parser.results.items()):
            # filter optional activities for automatic updates
            if self._auto and data[2] is True:
                logging.debug('filtered optional activity %s', bundle_id)
                continue

            bundle = registry.get_bundle(bundle_id)
            if bundle:
                if data[0] <= NormalizedVersion(bundle.get_activity_version()):
                    continue

            name = bundle.get_name() if bundle else None
            bundle_update = BundleUpdate(bundle_id,
                                         name,
                                         data[0],
                                         data[1],
                                         0,
                                         optional=data[2])
            self._bundles_to_check.append(bundle_update)
        self._total_bundles_to_check = len(self._bundles_to_check)
        _logger.debug("%d results after filter", self._total_bundles_to_check)
Пример #12
0
    def __data_json_download_complete_cb(self, downloader, result):
        if self._canceled:
            return

        try:
            activities = json.loads(result.get_data())['activities']
        except ValueError:
            self._error_cb('Can not parse loaded update.json')
            return

        updates = []

        for i, bundle in enumerate(self._bundles):
            self._progress_cb(bundle.get_name(), i/len(self._bundles))

            if bundle.get_bundle_id() not in activities:
                logging.debug('%s not in activities' % bundle.get_bundle_id())
                continue
            activity = activities[bundle.get_bundle_id()]

            try:
                version = NormalizedVersion(str(activity['version']))
                min_sugar = NormalizedVersion(str(activity['minSugarVersion']))
            except KeyError:
                logging.debug('KeyError - %s' % bundle.get_bundle_id())
                continue
            except InvalidVersionError:
                logging.debug('InvalidVersion - %s' % bundle.get_bundle_id())
                continue

            if NormalizedVersion(bundle.get_activity_version()) >= version:
                logging.debug('%s is up to date' % bundle.get_bundle_id())
                continue

            if NormalizedVersion(config.version) < min_sugar:
                logging.debug('Upgrade sugar for %s' % bundle.get_bundle_id())
                continue

            logging.debug('Marked for update: %s' % bundle.get_bundle_id())
            u = BundleUpdate(bundle.get_bundle_id(), bundle.get_name(),
                             version,
                             activity['xo_url'],
                             activity.get('xo_size', 1024 * 2))
            updates.append(u)

        self._completion_cb(updates)
Пример #13
0
    def __downloader_complete_cb(self, downloader, result):
        if isinstance(result, Exception):
            self.emit('check-complete', result)
            return

        if result is None:
            _logger.error('No XML update data returned from ASLO')
            return

        document = XML(result.get_data())

        if document.find(_FIND_DESCRIPTION) is None:
            _logger.debug(
                'Bundle %s not available in the server for the '
                'version %s', self._bundle.get_bundle_id(), config.version)
            version = None
            link = None
            size = None
            self.emit('check-complete', None)
            return

        try:
            version = NormalizedVersion(document.find(_FIND_VERSION).text)
        except InvalidVersionError:
            _logger.exception('Exception occurred while parsing version')
            self.emit('check-complete', None)
            return

        link = document.find(_FIND_LINK).text

        try:
            size = long(document.find(_FIND_SIZE).text) * 1024
        except ValueError:
            _logger.exception('Exception occurred while parsing size')
            size = 0

        if version > NormalizedVersion(self._bundle.get_activity_version()):
            result = BundleUpdate(self._bundle.get_bundle_id(),
                                  self._bundle.get_name(), version, link, size)
        else:
            result = None

        self.emit('check-complete', result)
Пример #14
0
    def _parse_info(self, info_file):
        cp = ConfigParser()
        cp.readfp(info_file)

        section = 'Activity'

        if cp.has_option(section, 'bundle_id'):
            self._bundle_id = cp.get(section, 'bundle_id')
        else:
            raise MalformedBundleException(
                'Activity bundle %s does not specify a bundle id' % self._path)

        if cp.has_option(section, 'name'):
            self._name = cp.get(section, 'name')
        else:
            raise MalformedBundleException(
                'Activity bundle %s does not specify a name' % self._path)

        if cp.has_option(section, 'exec'):
            self.bundle_exec = cp.get(section, 'exec')
        else:
            raise MalformedBundleException(
                'Activity bundle %s must specify either class or exec' %
                self._path)

        if cp.has_option(section, 'mime_types'):
            mime_list = cp.get(section, 'mime_types').strip(';')
            self._mime_types = [mime.strip() for mime in mime_list.split(';')]

        if cp.has_option(section, 'show_launcher'):
            if cp.get(section, 'show_launcher') == 'no':
                self._show_launcher = False

        if cp.has_option(section, 'tags'):
            tag_list = cp.get(section, 'tags').strip(';')
            self._tags = [tag.strip() for tag in tag_list.split(';')]

        if cp.has_option(section, 'icon'):
            self._icon = cp.get(section, 'icon')

        if cp.has_option(section, 'activity_version'):
            version = cp.get(section, 'activity_version')
            try:
                NormalizedVersion(version)
            except InvalidVersionError:
                raise MalformedBundleException(
                    'Activity bundle %s has invalid version number %s' %
                    (self._path, version))
            self._activity_version = version

        if cp.has_option(section, 'summary'):
            self._summary = cp.get(section, 'summary')
Пример #15
0
    def _do_work(self, task):
        bundle = task.bundle
        bundle_id = bundle.get_bundle_id()
        act = self._registry.get_bundle(bundle_id)
        logging.debug("InstallQueue task %s installed %r", bundle_id, act)

        if act:
            # Same version already installed?
            if act.get_activity_version() == bundle.get_activity_version():
                logging.debug('No upgrade needed, same version already '
                              'installed.')
                task.queue_callback(False)
                return

            # Would this new installation be a downgrade?
            if NormalizedVersion(bundle.get_activity_version()) <= \
                    NormalizedVersion(act.get_activity_version()) \
                    and not task.force_downgrade:
                task.queue_callback(AlreadyInstalledException())
                return

            # Uninstall the previous version, if we can
            if act.is_user_activity():
                try:
                    act.uninstall()
                except:
                    logging.exception('Uninstall failed, still trying to '
                                      'install newer bundle')
            else:
                logging.warning('Unable to uninstall system activity, '
                                'installing upgraded version in user '
                                'activities')

        try:
            task.queue_callback(bundle.install())
        except Exception, e:
            logging.debug("InstallThread install failed: %r", e)
            task.queue_callback(e)
Пример #16
0
    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
Пример #17
0
    def test_html_parser(self):
        parser = _UpdateHTMLParser("http://www.sugarlabs.org")
        fd = open(os.path.join(data_dir, "microformat.html"), "r")
        parser.feed(fd.read())
        parser.close()

        results = parser.results
        self.assertIn('org.sugarlabs.AbacusActivity', list(results.keys()))
        self.assertIn('org.laptop.WebActivity', list(results.keys()))

        # test that we picked the newest version
        version, url = results['org.sugarlabs.AbacusActivity']
        self.assertEqual(NormalizedVersion("43"), version)
        self.assertEqual("http://download.sugarlabs.org/abacus-43.xo", url)

        # test resolve relative url
        version, url = results['org.laptop.WebActivity']
        self.assertEqual("http://www.sugarlabs.org/browse-149.xo", url)
Пример #18
0
    def _parse_info(self, info_file):
        cp = ConfigParser()
        cp.readfp(info_file)

        section = 'Library'

        if cp.has_option(section, 'name'):
            self._name = cp.get(section, 'name')
        else:
            raise MalformedBundleException(
                'Content bundle %s does not specify a name' % self._path)

        if cp.has_option(section, 'library_version'):
            version = cp.get(section, 'library_version')
            try:
                NormalizedVersion(version)
            except InvalidVersionError:
                raise MalformedBundleException(
                    'Content bundle %s has invalid version number %s' %
                    (self._path, version))
            self._library_version = version

        if cp.has_option(section, 'locale'):
            self._locale = cp.get(section, 'locale')

        if cp.has_option(section, 'global_name'):
            self._global_name = cp.get(section, 'global_name')

        if cp.has_option(section, 'icon'):
            self._icon = cp.get(section, 'icon')

        # Compatibility with old content bundles
        if self._global_name is not None \
                and cp.has_option(section, 'bundle_class'):
            self._global_name = cp.get(section, 'bundle_class')

        if cp.has_option(section, 'activity_start'):
            self._activity_start = cp.get(section, 'activity_start')

        if self._global_name is None:
            raise MalformedBundleException(
                'Content bundle %s must specify global_name' % self._path)
Пример #19
0
    def __check_completed_cb(self, bundle, version, link, size, error_message):
        if error_message is not None:
            logging.error('Error getting update information from server:\n'
                          '%s' % error_message)

        if version is not None and \
                version > NormalizedVersion(bundle.get_activity_version()):
            self.updates.append(BundleUpdate(bundle, version, link, size))

        if self._cancelling:
            self._cancel_checking()
        elif self._bundles_to_check:
            GObject.idle_add(self._check_next_update)
        else:
            total = len(bundleregistry.get_registry())
            if bundle is None:
                name = ''
            else:
                name = bundle.get_name()
            self.emit('progress', UpdateModel.ACTION_CHECKING, name, total,
                      total)
Пример #20
0
    def handle_data(self, data):
        if self.in_group_name:
            self.group_name = data.strip()

        if self.in_group_desc:
            self.group_desc = data.strip()

        if self.in_activity_id > 0:
            if self.last_id is None:
                self.last_id = data.strip()
            else:
                self.last_id = self.last_id + data.strip()

        if self.in_activity_version > 0:
            try:
                self.last_version = NormalizedVersion(data.strip())
            except InvalidVersionError:
                pass

        if self.in_activity_optional > 0:
            # a value 1 means that this activity is optional
            self.last_optional = data.strip() == '1'
Пример #21
0
    def _parse_info(self, info_file):
        cp = ConfigParser()
        cp.readfp(info_file)

        section = 'Activity'

        if cp.has_option(section, 'bundle_id'):
            self._bundle_id = cp.get(section, 'bundle_id')
        else:
            if cp.has_option(section, 'service_name'):
                self._bundle_id = cp.get(section, 'service_name')
                logging.error('ATTENTION: service_name property in the '
                              'activity.info file is deprecated, should be '
                              ' changed to bundle_id')
            else:
                raise MalformedBundleException(
                    'Activity bundle %s does not specify a bundle id' %
                    self._path)

        if ' ' in self._bundle_id:
            raise MalformedBundleException('Space in bundle_id')

        if cp.has_option(section, 'name'):
            self._name = cp.get(section, 'name')
        else:
            raise MalformedBundleException(
                'Activity bundle %s does not specify a name' % self._path)

        if cp.has_option(section, 'exec'):
            self.bundle_exec = cp.get(section, 'exec')
        else:
            raise MalformedBundleException(
                'Activity bundle %s must specify either class or exec' %
                self._path)

        if cp.has_option(section, 'mime_types'):
            mime_list = cp.get(section, 'mime_types').strip(';')
            self._mime_types = [mime.strip() for mime in mime_list.split(';')]

        if cp.has_option(section, 'show_launcher'):
            if cp.get(section, 'show_launcher') == 'no':
                self._show_launcher = False

        if cp.has_option(section, 'tags'):
            tag_list = cp.get(section, 'tags').strip(';')
            self._tags = [tag.strip() for tag in tag_list.split(';')]

        if cp.has_option(section, 'icon'):
            self._icon = cp.get(section, 'icon')

        if cp.has_option(section, 'activity_version'):
            version = cp.get(section, 'activity_version')
            try:
                NormalizedVersion(version)
            except InvalidVersionError:
                raise MalformedBundleException(
                    'Activity bundle %s has invalid version number %s' %
                    (self._path, version))
            self._activity_version = version

        if cp.has_option(section, 'summary'):
            self._summary = cp.get(section, 'summary')

        if cp.has_option(section, 'single_instance'):
            if cp.get(section, 'single_instance') == 'yes':
                self._single_instance = True

        if cp.has_option(section, 'max_participants'):
            max_participants = cp.get(section, 'max_participants')
            try:
                self._max_participants = int(max_participants)
            except ValueError:
                raise MalformedBundleException(
                    'Activity bundle %s has invalid max_participants %s' %
                    (self._path, max_participants))
Пример #22
0
    def refresh(self,
                progress_callback=lambda n, extra: None,
                clear_cache=True):
        """Perform network operations to find available updates.

        The `progress_callback` is invoked with numbers between 0 and 1
        or `None` as the network queries complete.  The last callback will be
        `progress_callback(1)`.  Passing `None` to `progress_callback`
        requests "pulse mode" from the progress bar.
        """
        global _parse_cache
        if clear_cache:
            _parse_cache = {}  # clear microformat parse cache
            urlrange.urlcleanup()  # clean url cache
        self._cancel = False
        self._invalidate()
        # don't notify for the following; we'll notify at the end when we
        # know what the new values ought to be.
        self._saw_network_success = False
        self._network_failures = []
        # bookkeeping
        progress_callback(None, None)
        self.clear()
        # find all activities already installed.
        progress_callback(
            None, _('Looking for local activities and content...'))  # pulse
        activities = actinfo.get_activities() + actinfo.get_libraries()
        # enumerate all group urls
        progress_callback(None, _('Loading groups...'))
        group_urls = actinfo.get_activity_group_urls()
        # now we've got enough information to allow us to compute a
        # reasonable completion percentage.
        steps_total = [len(activities) + len(group_urls) + 3]
        steps_count = [0]  # box this to allow update from mkprog.

        def mkprog(msg=None):
            """Helper function to do progress update."""
            steps_count[0] += 1
            progress_callback(steps_count[0] / steps_total[0], msg)

        mkprog(_('Loading groups...'))
        # okay, first load up any group definitions; these take precedence
        # if present.
        groups = []

        def group_parser(f, url):
            name, desc, groups = microformat.parse_html(f.read(), url)
            if len(groups) > 0 or (name is not None and desc is not None):
                return name, desc, groups
            return None  # hmm, not a successful parse.

        for gurl in group_urls:
            mkprog(_('Fetching %s...') % gurl)
            if self._cancel: break  # bail!
            gdata = actinfo.retrieve_first_variant(gurl,
                                                   group_parser,
                                                   timeout=HTTP_TIMEOUT)
            if gdata is not None:
                gname, gdesc, gactmap = gdata
                groups.append((gname, gdesc, gurl, gactmap))
                self._saw_network_success = True
            else:
                # headers even for failed groups.
                groups.append((None, gurl, gurl, {}))
                self._network_failures.append(gurl)
        # now start filling up the liststore, keeping a map from activity id
        # to liststore path
        row_map = {}
        group_num = 0
        for gname, gdesc, gurl, gactmap in groups:
            # add group header.
            if gname is None: gname = _('Activity Group')
            self._append(IS_HEADER=True,
                         UPDATE_URL=gurl,
                         GROUP_NUM=group_num,
                         DESCRIPTION_BIG=gname,
                         DESCRIPTION_SMALL=gdesc)
            # now add entries for all activities in the group, whether
            # currently installed or not.
            for act_id, version_list in sorted(gactmap.items()):
                version, url = microformat.only_best_update(version_list)
                if act_id not in row_map:
                    # temporary description in case user cancels the refresh
                    tmp_desc = act_id.replace('sugar-is-lame',
                                              'lame-is-the-new-cool')
                    row_map[act_id] = self._append(ACTIVITY_ID=act_id,
                                                   GROUP_NUM=group_num,
                                                   UPDATE_EXISTS=True,
                                                   UPDATE_URL=url,
                                                   UPDATE_VERSION=str(version),
                                                   DESCRIPTION_BIG=tmp_desc)
                    steps_total[0] += 1  # new activity?
                else:
                    # allow for a later version in a different group
                    row = self[row_map[act_id]]
                    if NormalizedVersion(version) > \
                            NormalizedVersion(row[UPDATE_VERSION]):
                        row[UPDATE_URL] = url
                # XXX: deal with pinned updates.
            group_num += 1
        # add in information from local activities.
        self._append(IS_HEADER=True,
                     GROUP_NUM=group_num,
                     DESCRIPTION_BIG=_('Local activities'))
        for act in activities:
            act_id = act.get_bundle_id()
            if act_id not in row_map:
                row_map[act_id] = self._append(ACTIVITY_ID=act_id,
                                               GROUP_NUM=group_num,
                                               UPDATE_EXISTS=False)
            else:
                steps_total[0] -= 1  # correct double-counting.
            # update icon, and bundle
            row = self[row_map[act_id]]
            row[ACTIVITY_BUNDLE] = act
            row[DESCRIPTION_BIG] = act.get_name()
            if not self._skip_icons:
                try:
                    row[ACTIVITY_ICON] = _svg2pixbuf(act.get_icon_data())
                except IOError:
                    # dlo trac #8149: don't kill updater if existing icon
                    # bundle is malformed.
                    pass
        group_num += 1

        # now do extra network traffic to look for actual updates.
        def refresh_existing(row):
            """Look for updates to an existing activity."""
            act = row[ACTIVITY_BUNDLE]
            oldver = 0 if _DEBUG_MAKE_ALL_OLD else act.get_activity_version()
            size = 0

            def net_good(url_):
                self._saw_network_success = True

            def net_bad(url):
                self._network_failures.append(url)

            # activity group entries have UPDATE_EXISTS=True
            # for any activities not present in the group, try their update_url
            # (if any) for new updates
            # note the behaviour here: if the XS (which hosts activity groups)
            # has an entry for the activity, then we trust that it is the
            # latest and we don't go online to check.
            # we only go online for activities which the XS does not know about
            # the purpose of this is to reduce the high latency of having
            # to check multiple update_urls on a slow connection.

            if row[UPDATE_EXISTS]:
                # trust what the XS told us
                newver, newurl = row[UPDATE_VERSION], row[UPDATE_URL]
            else:
                # hit the internet for updates
                oldver, newver, newurl, size = \
                    _retrieve_update_version(act, net_good, net_bad)

            # make sure that the version we found is actually newer...
            if newver is not None and NormalizedVersion(newver) <= \
                    NormalizedVersion(act.get_activity_version()):
                newver = None
            elif row[UPDATE_EXISTS]:
                # since we trusted the activity group page above, we don't
                # know the size of this bundle. but if we're about to offer it
                # as an update then we should look that up now, with an HTTP
                # request.
                # (by avoiding a load of HTTP requests on activity versions that
                #  we already have, we greatly increase the speed and usability
                #  of this updater on high-latency connections)
                size = urlrange.urlopen(row[UPDATE_URL], timeout=HTTP_TIMEOUT)\
                       .length()

            row[UPDATE_EXISTS] = (newver is not None)
            row[UPDATE_URL] = newurl
            row[UPDATE_SIZE] = size
            if newver is None:
                description = _('At version %s') % oldver
            else:
                description = \
                    _('From version %(old)s to %(new)s (Size: %(size)s)') % \
                    { 'old':oldver, 'new':newver, 'size':_humanize_size(size) }
                row[UPDATE_SELECTED] = True
            row[DESCRIPTION_SMALL] = description

        def refresh_new(row):
            """Look for updates to a new activity in the group."""
            uo = urlrange.urlopen(row[UPDATE_URL], timeout=HTTP_TIMEOUT)
            row[UPDATE_SIZE] = uo.length()
            zf = zipfile.ZipFile(uo)
            # grab data from activity.info file
            activity_base = actutils.bundle_base_from_zipfile(zf)
            try:
                zf.getinfo('%s/activity/activity.info' % activity_base)
                is_activity = True
            except KeyError:
                try:
                    zf.getinfo('%s/library/library.info' % activity_base)
                    is_activity = False
                except:
                    raise RuntimeError("not activity or library")
            if is_activity:
                cp = actutils.activity_info_from_zipfile(zf)
                SECTION = 'Activity'
            else:
                cp = actutils.library_info_from_zipfile(zf)
                SECTION = 'Library'
            act_id = None
            for fieldname in ('bundle_id', 'service_name', 'global_name'):
                if cp.has_option(SECTION, fieldname):
                    act_id = cp.get(SECTION, fieldname)
                    break
            if not act_id:
                raise RuntimeError("bundle_id not found for %s" %
                                   row[UPDATE_URL])
            name = act_id
            if cp.has_option(SECTION, 'name'):
                name = cp.get(SECTION, 'name')
            # okay, try to get an appropriately translated name.
            if is_activity:
                lcp = actutils.locale_activity_info_from_zipfile(zf)
                if lcp is not None:
                    name = lcp.get(SECTION, 'name')
            else:
                s = actutils.locale_section_for_content_bundle(cp)
                if s is not None and cp.has_option(s, 'name'):
                    name = cp.get(s, 'name')
            version = None
            for fieldname in ('activity_version', 'library_version'):
                if cp.has_option(SECTION, fieldname):
                    version = cp.get(SECTION, fieldname)
                    break
            if version is None:
                raise RuntimeError("can't find version for %s" %
                                   row[UPDATE_URL])
            row[DESCRIPTION_BIG] = name
            row[UPDATE_SELECTED] = False
            row[DESCRIPTION_SMALL] = \
                _('New version %(version)s (Size: %(size)s)') % \
                {'version':version, 'size':_humanize_size(row[UPDATE_SIZE])}
            # okay, let's try to update the icon!
            if not self._skip_icons:
                if is_activity:
                    # XXX should failures here kill the upgrade?
                    icon_file = cp.get(SECTION, 'icon')
                    icon_filename = '%s/activity/%s.svg' % (activity_base,
                                                            icon_file)
                    row[ACTIVITY_ICON] = _svg2pixbuf(zf.read(icon_filename))
                else:
                    row[ACTIVITY_ICON] = _svg2pixbuf(
                        actinfo.DEFAULT_LIBRARY_ICON)

        # go through activities and do network traffic
        for row in self:
            if self._cancel: break  # bail!
            if row[IS_HEADER]: continue  # skip
            # skip journal
            if row[ACTIVITY_ID] == "org.laptop.JournalActivity": continue
            mkprog(_('Checking %s...') % row[DESCRIPTION_BIG])
            try:
                if row[ACTIVITY_BUNDLE] is None:
                    refresh_new(row)
                    self._saw_network_success = True
                else:
                    refresh_existing(row)
            except:
                row[UPDATE_EXISTS] = False  # something wrong, can't update
                if row[UPDATE_URL] is not None:
                    self._network_failures.append(row[UPDATE_URL])
                # log the problem for later debugging.
                print "Failure updating", row[DESCRIPTION_BIG], \
                      row[DESCRIPTION_SMALL], row[UPDATE_URL]
                traceback.print_exc()
        mkprog('Sorting...')  # all done
        # hide headers if all children are hidden
        sawone, last_header = False, None
        for row in self:
            if row[IS_HEADER]:
                if last_header is not None:
                    last_header[UPDATE_EXISTS] = sawone
                sawone, last_header = False, row
            elif row[UPDATE_EXISTS]:
                sawone = True
        if last_header is not None:
            last_header[UPDATE_EXISTS] = sawone
        # finally, sort all rows.
        self._sort()
        mkprog()  # all done
        # XXX: check for base os update, and add an entry here?
        self._is_valid = True
        self.notify('is-valid')
        self.notify('saw-network-failure')
        self.notify('saw-network-success')