changeset 0:6884bb8130ca

Initial revision
author Guido Berhoerster <guido+pui@berhoerster.name>
date Sun, 20 May 2018 11:32:57 +0200
parents
children 2f04ec9e0506
files Makefile README deps.sed docbook-update-source-data.xsl org.guido-berhoerster.code.package-update-indicator.desktop.in org.guido-berhoerster.code.package-update-indicator.gschema.xml package-update-indicator.1.xml package-update-indicator.c po/LINGUAS po/POTFILES.in pui-application.c pui-application.h pui-backend.c pui-backend.h pui-common.h pui-get-updates.c pui-get-updates.h pui-types.c pui-types.h
diffstat 18 files changed, 2276 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,204 @@
+#
+# Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+PACKAGE =	package-update-indicator
+APPLICATION_ID = org.guido-berhoerster.code.package-update-indicator
+VERSION =	1
+DISTNAME =	$(PACKAGE)-$(VERSION)
+AUTHOR =	Guido Berhoerster
+BUG_ADDRESS =	guido+pui@berhoerster.name
+
+# gcc, clang, icc, Sun/Solaris Studio
+CC :=		$(CC) -std=c99
+COMPILE.c =	$(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS) $(TARGET_ARCH) -c
+# gcc, clang, icc
+MAKEDEPEND.c =	$(CC) -MM $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS)
+# Sun/Solaris Studio
+#MAKEDEPEND.c =	$(CC) -xM1 $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS)
+# X makedepend
+#MAKEDEPEND.c =	makedepend -f- -Y -- $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS) --
+LINK.c =	$(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(XCPPFLAGS) $(LDFLAGS) $(XLDFLAGS) $(TARGET_ARCH)
+LINK.o =	$(CC) $(LDFLAGS) $(XLDFLAGS) $(TARGET_ARCH)
+CP :=		cp
+INSTALL :=	install
+INSTALL.exec :=	$(INSTALL) -D -m 0755
+INSTALL.data :=	$(INSTALL) -D -m 0644
+INSTALL.link :=	$(CP) -f -P
+PAX :=		pax
+GZIP :=		gzip
+SED :=		sed
+GLIB_COMPILE_SCHEMAS := $(shell pkg-config --variable=glib_compile_schemas gio-2.0)
+XSLTPROC :=	xsltproc
+DOCBOOK5_MANPAGES_STYLESHEET = http://docbook.sourceforge.net/release/xsl-ns/current/manpages/docbook.xsl
+DOCBOOK5_MANPAGES_FLAGS = --stringparam man.authors.section.enabled 0 \
+		--stringparam man.copyright.section.enabled 0
+MSGFMT =	msgfmt
+MSGMERGE =	msgmerge
+XGETTEXT =	xgettext
+XGETTEXT_OPTIONS = --copyright-holder "$(AUTHOR)" \
+		--package-name '$(PACKAGE)' \
+		--package-version '$(VERSION)' \
+		--msgid-bugs-address '$(BUG_ADDRESS)' \
+		--default-domain '$(PACKAGE)' \
+		--from-code UTF-8 \
+		--keyword=_ \
+		--keyword=N_ \
+		--keyword=C_:1c,2 \
+		--keyword=NC_:1c,2 \
+		--keyword=g_dngettext:2,3 \
+		--add-comments
+
+define generate-manpage-rule =
+%.$1: %.$(addsuffix .xml,$1) docbook-update-source-data.xsl
+	$$(XSLTPROC) \
+	    --xinclude \
+	    --stringparam package $$(PACKAGE) \
+	    --stringparam version $$(VERSION) \
+	    docbook-update-source-data.xsl $$< | \
+	    $$(XSLTPROC) \
+	    --xinclude \
+	    --output $$@ \
+	    $$(DOCBOOK5_MANPAGES_FLAGS) \
+	    $$(DOCBOOK5_MANPAGES_STYLESHEET) \
+	    -
+endef
+
+DESTDIR ?=
+prefix ?=	/usr/local
+bindir ?=	$(prefix)/bin
+datadir ?=	$(prefix)/share
+mandir ?=	$(datadir)/man
+localedir ?=	$(datadir)/locale
+sysconfdir ?=	/etc
+xdgautostartdir ?= $(sysconfdir)/xdg/autostart
+
+OS_NAME :=	$(shell uname -s)
+OS_RELEASE :=	$(shell uname -r)
+
+OBJS =		package-update-indicator.o \
+		pui-application.o \
+		pui-backend.o \
+		pui-get-updates.o \
+		pui-types.o
+
+GSETTINGS_SCHEMAS = $(APPLICATION_ID).gschema.xml
+
+AUTOSTART_FILES = $(APPLICATION_ID).desktop
+
+LINGUAS :=	$(shell sed 's/\#.*//' po/LINGUAS)
+MOFILES :=	$(patsubst %,po/%.mo,$(LINGUAS))
+POTFILES_IN :=	$(shell sed 's/\#.*//' po/POTFILES.in)
+POTFILE =	po/$(PACKAGE).pot
+
+MANPAGES =	$(PACKAGE).1
+
+.DEFAULT_TARGET = all
+
+.PHONY: all pot update-po clean clobber dist install
+
+all: $(PACKAGE) $(AUTOSTART_FILES) $(MOFILES) $(MANPAGES)
+
+$(PACKAGE): XCPPFLAGS =	-DPACKAGE=\"$(PACKAGE)\" \
+			-DAPPLICATION_ID=\"$(APPLICATION_ID)\" \
+			-DVERSION=\"$(VERSION)\" \
+			-DG_LOG_DOMAIN=\"$(PACKAGE)\" \
+			-DPACKAGE_LOCALE_DIR="\"$(localedir)\"" \
+			-DGETTEXT_PACKAGE=\"$(PACKAGE)\" \
+			-DI_KNOW_THE_PACKAGEKIT_GLIB2_API_IS_SUBJECT_TO_CHANGE
+$(PACKAGE): XCFLAGS =	$(shell pkg-config --cflags gtk+-3.0 \
+			    appindicator3-0.1 packagekit-glib2)
+$(PACKAGE): LDLIBS =	$(shell pkg-config --libs gtk+-3.0 \
+			    appindicator3-0.1 packagekit-glib2)
+
+ifneq ($(findstring $(OS_NAME),FreeBSD DragonFly OpenBSD),)
+  $(PACKAGE): XCPPFLAGS +=	-I/usr/local/include
+  $(PACKAGE): XLDFLAGS +=	-L/usr/local/lib
+else ifeq ($(OS_NAME),NetBSD)
+  $(PACKAGE): XCPPFLAGS +=	-I/usr/pkg/include
+  $(PACKAGE): XLDFLAGS +=	-L/usr/pkg/lib
+endif
+ifeq ($(findstring $(OS_NAME),FreeBSD DragonFly NetBSD OpenBSD),)
+  $(PACKAGE): XCPPFLAGS +=	-D_XOPEN_SOURCE=600
+endif
+
+$(PACKAGE): $(OBJS)
+	$(LINK.o) $^ $(LDLIBS) -o $@
+
+$(POTFILE): po/POTFILES.in $(POTFILES_IN)
+	$(XGETTEXT) $(XGETTEXT_OPTIONS) --files-from $< --output $@
+
+pot: $(POTFILE)
+
+update-po: $(POTFILE)
+	for pofile in $(patsubst %.mo,%.po,$(MOFILES)); do \
+	    $(MSGMERGE) --update --backup off $$pofile $<; \
+	done
+
+%.mo: %.po
+	$(MSGFMT) --output $@ $<
+
+%.desktop: %.desktop.in
+	$(MSGFMT) --desktop --template $< -d po --output $@
+
+$(foreach section,1 2 3 4 5 6 7 8 9,$(eval $(call generate-manpage-rule,$(section))))
+
+%.o: %.c
+	$(MAKEDEPEND.c) $< | $(SED) -f deps.sed >$*.d
+	$(COMPILE.c) -o $@ $<
+
+install: all
+	$(INSTALL.exec) $(PACKAGE) "$(DESTDIR)$(bindir)/$(PACKAGE)"
+	for schema in $(GSETTINGS_SCHEMAS); do \
+	    $(INSTALL.data) $${schema} \
+	        $(DESTDIR)$(datadir)/glib-2.0/schemas/$${schema}; \
+	done
+	[ -n "$(GSETTINGS_SCHEMAS)" ] && \
+	    $(GLIB_COMPILE_SCHEMAS) $(datadir)/glib-2.0/schemas
+	for desktop_file in $(AUTOSTART_FILES); do \
+	    $(INSTALL.data) $${desktop_file} \
+	        $(DESTDIR)$(xdgautostartdir)/$${desktop_file}; \
+	done
+	for lang in $(LINGUAS); do \
+	   $(INSTALL.data) po/$${lang}.mo \
+	        $(DESTDIR)$(localedir)/$${lang}/LC_MESSAGES/$(PACKAGE).mo; \
+	done
+	for manpage in $(MANPAGES); do \
+	    $(INSTALL.data) $${manpage} \
+	        "$(DESTDIR)$(mandir)/man$${manpage##*.}/$${manpage##*/}"; \
+	done
+
+clean:
+	rm -f $(PACKAGE) $(OBJS) $(AUTOSTART_FILES) $(POTFILE) $(MOFILES) \
+	    $(MANPAGES)
+
+clobber: clean
+	rm -f $(patsubst %.o,%.d,$(OBJS))
+
+dist: clobber
+	$(PAX) -w -x ustar -s ',.*/\..*,,' -s ',./[^/]*\.tar\.gz,,' \
+	    -s ',^\.$$,,' -s ',\./,$(DISTNAME)/,' . | \
+	    $(GZIP) > $(DISTNAME).tar.gz
+
+-include local.mk
+
+-include $(patsubst %.o,%.d,$(OBJS))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,107 @@
+package-update-indicator
+========================
+
+Description
+-----------
+
+The package-update-indicator utility regularly checks for software updates and
+notifies the user about available updates using desktop notifications and
+either a status notifier icon or a system tray icon.
+
+Build Instructions
+------------------
+
+package-update-indicator requires a POSIX:2004 compatible operating system, it
+has been tested to work on Linux distributions.  The following tools and
+shared libraries are required to build package-update-indicator:
+
+- GNU make 3.81 or later
+- pkg-config
+- GNU gettext 0.19 or later
+- GNU or BSD install
+- GLib version 2.48 or later
+- GTK+ version 3.18 or later
+- libappindicator 12.10.0 or later
+- PackageKit-glib2 0.8.17 or later
+- the xsltproc tool from libxml2
+
+Before building package-update-indicator check the commented macros in the
+Makefile for any macros you may need to override depending on the used
+toolchain and operating system.
+
+By default, all files will be installed under the "/usr/local" directory, a
+different installation path prefix can be set via the `prefix` macro.  In
+addition, a second path prefix can be specified via the `DESTDIR` macro which
+will be prepended to any path, incuding the `prefix` macro path prefix.  In
+contrast to `prefix`, the path specified via the `DESTDIR` macro will only be
+prepended to paths during installation and not be used for constructing
+internal paths.
+
+The following instructions assume that `make` is GNU make, on some platforms
+it may be installed under a different name or a non-default path.  In order to
+start the build process run `make all`.  After a successful build, run `make
+install` to install the program, any associated data files and the
+documentation.
+
+Previously built binaries, object files, generated data files and
+documentation can be removed by running `make clean`, any additional,
+generated files which are not removed by the `clean` target can be removed by
+running `make clobber`.
+
+Contact
+-------
+
+Please send any feedback, translations or bug reports via email to
+<guido+pui@berhoerster.name>.
+
+Bug Reports
+-----------
+
+When sending bug reports, please always mention the exact version of
+package-update-indicator with which the issue occurs as well as the version of
+the operating system you are using and make sure that you provide sufficient
+information to reproduce the issue and include any input, output, any error
+messages.
+
+In case of build issues, please also specify the implementations and versions
+of the tools and shared libraries used to build the program, in particular the
+compiler.
+
+In case of crashes, please generate a stack trace with a suitable debugger
+such as gdb, lldb, dbx, or debug after a crash has occurred either by
+examining the resulting core file or by running the program from the debugger
+and attach it to the bug report.  In order to generate a meaningful stack
+trace the program as well as any dynamically linked libraries need to be built
+with debugging information, see the documentation of the used compiler for the
+required compiler flags.  If any of the dynamically linked shared libraries do
+not contain debugging information, please either install debugging information
+for these libraries using mechanisms provided by your operating system or
+rebuild the libraries accordingly.  Please refer to the documentation of the
+debugger for detailed instructions on generating backtraces.
+
+License
+-------
+
+Except otherwise noted, all files are Copyright (C) 2018 Guido Berhoerster and
+distributed under the following license terms:
+
+Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/deps.sed	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,26 @@
+/^[^:]\{1,\}:.*\\$/{
+    h
+    s/\([^:]\{1,\}:\).*/\1/
+    x
+    s/[^:]\{1,\}://
+}
+/\\$/,/^$/bgen
+/\\$/,/[^\\]$/{
+:gen
+    s/[[:blank:]]*\\$//
+    s/^[[:blank:]]*//
+    G
+    s/\(.*\)\n\(.*\)/\2 \1/
+}
+/^[^:]\{1,\}:[[:blank:]]*$/d
+/^[^:]\{1,\}\.o:/{
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.[cC][[:blank:]]*/ /g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.[cC]$//g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cc[[:blank:]]*/ /g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cc$//g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cpp[[:blank:]]*/ /g
+    s/[[:blank:]]*[^[:blank:]]\{1,\}\.cpp$//g
+    /^[^:]\{1,\}:[[:blank:]]*$/d
+    s/^\([^:]\{1,\}\)\.o[[:blank:]]*:[[:blank:]]*\(.*\)/\1.d: $(wildcard \2)\
+&/
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docbook-update-source-data.xsl	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<xsl:stylesheet
+  version="1.0"
+  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+  xmlns:db="http://docbook.org/ns/docbook"
+  xmlns="http://docbook.org/ns/docbook"
+  exclude-result-prefixes="xsl db">
+
+  <xsl:param name="package" select="''" />
+  <xsl:param name="version" select="''" />
+
+  <xsl:template match="db:refmeta/db:refmiscinfo[@class = 'source' or
+    @class = 'version']"/>
+
+  <xsl:template match="db:refmeta">
+    <xsl:copy>
+      <xsl:apply-templates/>
+      <refmiscinfo class="source"><xsl:value-of select="$package"/></refmiscinfo>
+      <refmiscinfo class="version"><xsl:value-of select="$version"/></refmiscinfo>
+    </xsl:copy>
+  </xsl:template>
+
+  <xsl:template match="@*|node()">
+    <xsl:copy>
+      <xsl:apply-templates select="@*|node()"/>
+    </xsl:copy>
+  </xsl:template>
+
+</xsl:stylesheet>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/org.guido-berhoerster.code.package-update-indicator.desktop.in	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Name=Package Update Indicator
+GenericName=Package Update Indicator
+Comment=Notify about available software updates
+Icon=system-software-update
+Exec=package-update-indicator
+Terminal=false
+Type=Application
+Categories=Utility;GTK;TrayIcon;
+NotShowIn=GNOME;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/org.guido-berhoerster.code.package-update-indicator.gschema.xml	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,17 @@
+<schemalist>
+  <schema id="org.guido-berhoerster.code.package-update-indicator"
+  path="/org/guido-berhoerster/code/package-update-indicator/"
+  gettext-domain="package-update-indicator">
+    <key name="update-command" type="s">
+      <default>""</default>
+      <summary>Update command</summary>
+      <description>Command for installing updates.</description>
+    </key>
+    <key name="refresh-interval" type="u">
+      <default>86400</default>
+      <summary>Refresh interval</summary>
+      <description>The interval in seconds for refreshing
+      metadata.</description>
+    </key>
+  </schema>
+</schemalist>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/package-update-indicator.1.xml	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,130 @@
+<?xml version="1.0"?>
+<!--
+
+Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-->
+<refentry xmlns="http://docbook.org/ns/docbook" xml:lang="en">
+  <info>
+    <author>
+      <personname>
+        <firstname>Guido</firstname>
+        <surname>Berhoerster</surname>
+      </personname>
+      <email>guido+pui@berhoerster.name</email>
+      <personblurb/>
+    </author>
+    <date>25 May, 2018</date>
+  </info>
+  <refmeta>
+    <refentrytitle>package-update-indicator</refentrytitle>
+    <manvolnum>1</manvolnum>
+    <refmiscinfo class="source"/>
+    <refmiscinfo class="version"/>
+    <refmiscinfo class="manual">User Commands</refmiscinfo>
+  </refmeta>
+  <refnamediv>
+    <refname>package-update-indicator</refname>
+    <refpurpose>notify about available software updates</refpurpose>
+  </refnamediv>
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>package-update-indicator</command>
+      <group>
+        <arg choice="plain">
+          <option>-q</option>
+        </arg>
+        <arg choice="plain">
+          <option>--quit</option>
+        </arg>
+      </group>
+      <group>
+        <arg choice="plain">
+          <option>-V</option>
+        </arg>
+        <arg choice="plain">
+          <option>--version</option>
+        </arg>
+      </group>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+  <refsect1>
+    <title>Description</title>
+    <para>The <command>package-update-indicator</command> utility regularly
+    checks for software updates and notifies the user about available updates
+    using desktop notifications and either a status notifier icon or a system
+    tray icon.</para>
+  </refsect1>
+  <refsect1>
+    <title>Options</title>
+    <para>The following options are supported:</para>
+    <variablelist>
+      <varlistentry>
+        <term>
+          <option>-q</option>
+        </term>
+        <term>
+          <option>--quit</option>
+        </term>
+        <listitem>
+          <para>Quit the running instance of
+          <command>package-update-indicator</command>.</para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>
+          <option>-V</option>
+        </term>
+        <term>
+          <option>--version</option>
+        </term>
+        <listitem>
+          <para>Print the version number and exit.</para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+  <refsect1>
+    <title>Exit Status</title>
+    <para>The following exit values are returned:</para>
+    <variablelist>
+      <varlistentry>
+        <term>0</term>
+        <listitem>
+          <para>Command successfully executed.</para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>1</term>
+        <listitem>
+          <para>An unspecified error has occured.</para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term>2</term>
+        <listitem>
+          <para>Invalid command line options were specified.</para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+</refentry>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/package-update-indicator.c	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include <locale.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "pui-application.h"
+
+gboolean	restart;
+
+int
+main(int argc, char *argv[])
+{
+	int		status;
+	gchar		*program;
+	PuiApplication	*application;
+
+	/* try to obtain the name of the executable for safe re-execution */
+	if (argv[0] == NULL) {
+		g_error("unable to locate %s executable", PACKAGE);
+	}
+	if (argv[0][0] != '/') {
+		program = g_find_program_in_path(argv[0]);
+		if (program == NULL) {
+			g_error("unable to locate %s executable", PACKAGE);
+		}
+		argv[0] = program;
+	}
+
+	bindtextdomain(GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR);
+	setlocale(LC_ALL, "");
+	bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+	textdomain(GETTEXT_PACKAGE);
+
+	g_set_application_name(_("Package Update Indicator"));
+
+	gtk_init(&argc, &argv);
+
+	application = pui_application_new();
+	status = g_application_run(G_APPLICATION(application), argc, argv);
+	g_object_unref(application);
+
+	if (restart) {
+		/* application restart requested */
+		if (execv(argv[0], argv) == -1) {
+			g_error("exec: %s", g_strerror(errno));
+		}
+	}
+
+	exit(status);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/po/POTFILES.in	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,7 @@
+org.guido-berhoerster.code.package-update-indicator.desktop.in
+org.guido-berhoerster.code.package-update-indicator.gschema.xml
+package-update-indicator.c
+pui-application.c
+pui-backend.c
+pui-get-updates.c
+pui-types.c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pui-application.c	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,680 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#define	G_SETTINGS_ENABLE_BACKEND
+#include <gio/gsettingsbackend.h>
+#include <glib/gi18n.h>
+#include <libappindicator/app-indicator.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "pui-common.h"
+#include "pui-application.h"
+#include "pui-types.h"
+#include "pui-backend.h"
+
+#define	SETTINGS_ROOT_PATH \
+    "/org/guido-berhoerster/code/package-update-indicator/"
+#define	SETTINGS_ROOT_GROUP	"General"
+
+struct _PuiApplication {
+	GApplication	parent_instance;
+	GSettings	*settings;
+	GCancellable	*cancellable;
+	PuiBackend	*backend;
+	AppIndicator	*indicator;
+	GtkWidget	*about_dialog;
+	GIcon		*icons[PUI_STATE_LAST];
+	PuiState	state;
+	gchar		*update_command;
+	gchar		*error_message;
+};
+
+G_DEFINE_TYPE(PuiApplication, pui_application, G_TYPE_APPLICATION)
+
+enum {
+    PROP_0,
+    PROP_UPDATE_COMMAND,
+    PROP_LAST
+};
+
+extern gboolean	restart;
+
+static GParamSpec *properties[PROP_LAST] = { NULL };
+
+static const gchar *icon_names[PUI_STATE_LAST] = {
+    [PUI_STATE_INITIAL] = "system-software-update",
+    [PUI_STATE_UP_TO_DATE] = "system-software-update",
+    [PUI_STATE_NORMAL_UPDATES_AVAILABLE] = "software-update-available",
+    [PUI_STATE_IMPORTANT_UPDATES_AVAILABLE] = "software-update-urgent",
+    [PUI_STATE_ERROR] = "dialog-warning"
+};
+
+static const GOptionEntry cmd_options[] = {
+    { "debug", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
+    N_("Enable debugging messages"), NULL },
+    { "quit", 'q', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
+    N_("Quit running instance of Package Update Indicator"), NULL },
+    { "version", 'V', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL,
+    N_("Print the version number and quit"), NULL },
+    { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, NULL, NULL, NULL },
+    { NULL }
+};
+
+static void	pui_application_show_about_dialog(GSimpleAction *, GVariant *,
+    gpointer);
+static void	pui_application_quit(GSimpleAction *, GVariant *, gpointer);
+static void	pui_application_install_updates(GSimpleAction *, GVariant *,
+    gpointer);
+
+static const GActionEntry pui_application_actions[] = {
+    { "about", pui_application_show_about_dialog },
+    { "quit", pui_application_quit },
+    { "install-updates", pui_application_install_updates }
+};
+
+static gboolean
+program_exists(const gchar *command_line)
+{
+	gboolean	is_program_in_path;
+	gchar		**argv = NULL;
+	gchar		*program_path;
+
+	if (!g_shell_parse_argv(command_line, NULL, &argv, NULL)) {
+		return (FALSE);
+	}
+	program_path = g_find_program_in_path(argv[0]);
+	is_program_in_path = (program_path != NULL) ? TRUE : FALSE;
+	g_free(program_path);
+	g_strfreev(argv);
+
+	return (is_program_in_path);
+}
+
+static void
+pui_application_show_about_dialog(GSimpleAction *simple, GVariant *parameter,
+    gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+	const gchar	*translators;
+
+	if (self->about_dialog == NULL) {
+		translators = _("Translators");
+		if (strcmp(translators, "Translators") == 0) {
+			translators = NULL;
+		}
+
+		self->about_dialog = gtk_about_dialog_new();
+		g_object_set(G_OBJECT(self->about_dialog),
+		    "authors", (gchar *[]){ "Guido Berhoerster", NULL },
+		    "comments", _("Notify about available software updates"),
+		    "copyright", "Copyright \xc2\xa9 2018 Guido Berhoerster",
+		    "license-type", GTK_LICENSE_MIT_X11,
+		    "logo-icon-name", "system-software-update",
+		    "translator-credits", translators,
+		    "version", VERSION,
+		    "website", "https://code.guido-berhoerster.org/projects/"
+		    "package-update-indicator/",
+		    "wrap-license", TRUE,
+		    NULL);
+	}
+
+	gtk_dialog_run(GTK_DIALOG(self->about_dialog));
+	gtk_widget_hide(self->about_dialog);
+}
+
+static void
+pui_application_quit(GSimpleAction *simple, GVariant *parameter,
+    gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+
+	/* quit the GTK main loop if the about dialog is running */
+	if (self->about_dialog != NULL) {
+		gtk_widget_hide(self->about_dialog);
+	}
+
+	g_application_quit(G_APPLICATION(self));
+}
+
+static void
+pui_application_install_updates(GSimpleAction *simple, GVariant *parameter,
+    gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+	GError		*error = NULL;
+
+	if (!g_spawn_command_line_async(self->update_command, &error)) {
+		g_warning("failed to run update command: %s", error->message);
+		g_error_free(error);
+	}
+}
+
+static void
+update_ui(PuiApplication *self)
+{
+	GSimpleAction	*install_updates_action;
+	guint		important_updates = 0;
+	guint		normal_updates = 0;
+	gchar		*title = NULL;
+	gchar		*body = NULL;
+	GApplication	*application = G_APPLICATION(self);
+	GNotification	*notification = NULL;
+
+	install_updates_action =
+	    G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(self),
+	    "install-updates"));
+
+	if ((self->state == PUI_STATE_NORMAL_UPDATES_AVAILABLE) ||
+	    (self->state == PUI_STATE_IMPORTANT_UPDATES_AVAILABLE)) {
+		g_object_get(self->backend,
+		    "important-updates", &important_updates,
+		    "normal-updates", &normal_updates, NULL);
+	}
+
+	/* actions */
+	switch (self->state) {
+	case PUI_STATE_INITIAL:				/* FALLTHGROUGH */
+	case PUI_STATE_UP_TO_DATE:			/* FALLTHGROUGH */
+	case PUI_STATE_ERROR:
+		g_simple_action_set_enabled(install_updates_action, FALSE);
+		break;
+	case PUI_STATE_NORMAL_UPDATES_AVAILABLE:	/* FALLTHROUGH */
+	case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE:
+		g_simple_action_set_enabled(install_updates_action,
+		    program_exists(self->update_command));
+		break;
+	}
+
+	/* title and body for indicator and notification */
+	switch (self->state) {
+	case PUI_STATE_INITIAL:
+		title = g_strdup("");
+		body = g_strdup("");
+		break;
+	case PUI_STATE_UP_TO_DATE:
+		title = g_strdup(_("Up to Date"));
+		body = g_strdup(_("The system is up to date."));
+		break;
+	case PUI_STATE_NORMAL_UPDATES_AVAILABLE:
+		title = g_strdup(g_dngettext(NULL, "Software Update",
+		    "Software Updates", (gulong)normal_updates));
+		if (normal_updates == 1) {
+			body = g_strdup(_("There is a software update "
+			    "avaliable."));
+		} else {
+			body = g_strdup_printf(_("There are %u "
+			    "software updates avaliable."), normal_updates);
+		}
+		break;
+	case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE:
+		title = g_strdup(g_dngettext(NULL, "Important Software Update",
+		    "Important Software Updates", (gulong)important_updates));
+		if ((normal_updates == 0) && (important_updates == 1)) {
+			body = g_strdup(_("There is an important "
+			    "software update available."));
+		} else if ((normal_updates == 0) && (important_updates > 1)) {
+			body = g_strdup_printf(_("There are %u "
+			    "important software updates available."),
+			    important_updates);
+		} else if ((normal_updates > 0) && (important_updates == 1)) {
+			body = g_strdup_printf(_("There are %u "
+			    "software updates available, "
+			    "one of them is important."),
+			    normal_updates + important_updates);
+		} else {
+			body = g_strdup_printf(_("There are %u "
+			    "software updates available, "
+			    "%u of them are important."),
+			    normal_updates + important_updates,
+			    important_updates);
+		}
+		break;
+	case PUI_STATE_ERROR:
+		title = g_strdup(self->error_message);
+		break;
+	}
+
+	/* indicator */
+	switch (self->state) {
+	case PUI_STATE_INITIAL:
+		app_indicator_set_status(self->indicator,
+		    APP_INDICATOR_STATUS_PASSIVE);
+		break;
+	case PUI_STATE_UP_TO_DATE:			/* FALLTHGROUGH */
+	case PUI_STATE_NORMAL_UPDATES_AVAILABLE:	/* FALLTHGROUGH */
+	case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE:	/* FALLTHGROUGH */
+	case PUI_STATE_ERROR:
+		app_indicator_set_status(self->indicator,
+		    APP_INDICATOR_STATUS_ACTIVE);
+		break;
+	}
+	app_indicator_set_icon_full(self->indicator, icon_names[self->state],
+	    title);
+
+	/* notification */
+	switch (self->state) {
+	case PUI_STATE_INITIAL:				/* FALLTHGROUGH */
+	case PUI_STATE_UP_TO_DATE:			/* FALLTHGROUGH */
+	case PUI_STATE_ERROR:
+		/* withdraw exisiting notification */
+		g_application_withdraw_notification(application,
+		    "package-updates");
+		break;
+	case PUI_STATE_NORMAL_UPDATES_AVAILABLE:	/* FALLTHGROUGH */
+	case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE:
+		/* create notification */
+		notification = g_notification_new(title);
+		g_notification_set_body(notification, body);
+		g_notification_set_icon(notification, self->icons[self->state]);
+		g_notification_set_priority(notification,
+		    G_NOTIFICATION_PRIORITY_NORMAL);
+		if (g_action_get_enabled(G_ACTION(install_updates_action))) {
+			g_notification_add_button(notification,
+			    _("Install Updates"),
+			    "app.install-updates");
+		}
+		g_application_send_notification(application, "package-updates",
+		    notification);
+		break;
+	}
+
+	if (notification != NULL) {
+		g_object_unref(notification);
+	}
+
+	g_debug("indicator icon: %s, notification title: \"%s\", "
+	    "notification body: \"%s\"", icon_names[self->state], title, body);
+
+	g_free(body);
+	g_free(title);
+}
+
+static void
+transition_state(PuiApplication *self)
+{
+	PuiState	state = self->state;
+	guint		important_updates;
+	guint		normal_updates;
+	gchar		*old_state;
+	gchar		*new_state;
+
+	switch (self->state) {
+	case PUI_STATE_INITIAL:				/* FALLTHROUGH */
+	case PUI_STATE_UP_TO_DATE:			/* FALLTHROUGH */
+	case PUI_STATE_NORMAL_UPDATES_AVAILABLE:	/* FALLTHROUGH */
+	case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE:
+		if (self->error_message != NULL) {
+			state = PUI_STATE_ERROR;
+			break;
+		}
+
+		g_object_get(self->backend,
+		    "important-updates", &important_updates,
+		    "normal-updates", &normal_updates, NULL);
+		if (important_updates > 0) {
+			state = PUI_STATE_IMPORTANT_UPDATES_AVAILABLE;
+		} else if (normal_updates > 0) {
+			state = PUI_STATE_NORMAL_UPDATES_AVAILABLE;
+		} else {
+			state = PUI_STATE_UP_TO_DATE;
+		}
+		break;
+	case PUI_STATE_ERROR:
+		break;
+	}
+
+	if (state != self->state) {
+		old_state = pui_types_enum_to_string(PUI_TYPE_STATE,
+		    self->state);
+		new_state = pui_types_enum_to_string(PUI_TYPE_STATE, state);
+		g_debug("state %s -> %s", old_state, new_state);
+
+		self->state = state;
+		update_ui(self);
+
+		g_free(new_state);
+		g_free(old_state);
+	}
+}
+
+static void
+on_backend_restart_required(PuiBackend *backend, gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+
+	restart = TRUE;
+	g_action_group_activate_action(G_ACTION_GROUP(G_APPLICATION(self)),
+	    "quit", NULL);
+}
+
+static void
+on_backend_state_changed(PuiBackend *backend, gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+
+	transition_state(self);
+}
+
+static void
+on_pui_backend_finished(GObject *source_object, GAsyncResult *result,
+    gpointer user_data)
+{
+	PuiApplication	*self = user_data;
+	GError		*error = NULL;
+
+	self->backend = pui_backend_new_finish(result, &error);
+	if (self->backend == NULL) {
+		g_warning("failed to instantiate backend: %s", error->message);
+		g_free(self->error_message);
+		g_error_free(error);
+		self->error_message = g_strdup(_("Update notifications "
+		    "are not supported."));
+		transition_state(self);
+		return;
+	}
+
+	g_settings_bind(self->settings, "refresh-interval", self->backend,
+	    "refresh-interval", G_SETTINGS_BIND_GET);
+
+	transition_state(self);
+
+	g_signal_connect(self->backend, "restart-required",
+	    G_CALLBACK(on_backend_restart_required), self);
+	g_signal_connect(self->backend, "state-changed",
+	    G_CALLBACK(on_backend_state_changed), self);
+}
+
+static void
+pui_application_startup(GApplication *application)
+{
+	PuiApplication	*self = PUI_APPLICATION(application);
+	gsize		i;
+	gchar		*settings_filename;
+	GSettingsBackend *settings_backend;
+	GtkWidget	*menu;
+	GtkWidget	*menu_item;
+
+	G_APPLICATION_CLASS(pui_application_parent_class)->startup(application);
+
+	/* create actions */
+	g_action_map_add_action_entries(G_ACTION_MAP(self),
+	    pui_application_actions, G_N_ELEMENTS(pui_application_actions),
+	    self);
+
+	/* load icons */
+	for (i = 0; i < G_N_ELEMENTS(self->icons); i++) {
+		self->icons[i] = g_themed_icon_new(icon_names[i]);
+	}
+
+	/* create settings */
+	settings_filename = g_build_filename(g_get_user_config_dir(), PACKAGE,
+	    PACKAGE ".conf", NULL);
+	settings_backend = g_keyfile_settings_backend_new(settings_filename,
+	    SETTINGS_ROOT_PATH, SETTINGS_ROOT_GROUP);
+	self->settings = g_settings_new_with_backend(APPLICATION_ID,
+	    settings_backend);
+	g_settings_bind(self->settings, "update-command", self,
+	    "update-command", G_SETTINGS_BIND_GET);
+
+	/* start instantiating backend */
+	pui_backend_new_async(self->cancellable, on_pui_backend_finished, self);
+
+	/* create indicator */
+	self->indicator = app_indicator_new(APPLICATION_ID,
+	    "system-software-update",
+	    APP_INDICATOR_CATEGORY_APPLICATION_STATUS);
+
+	/* build menu */
+	menu = gtk_menu_new();
+	gtk_widget_insert_action_group(GTK_WIDGET(menu), "app",
+	    G_ACTION_GROUP(self));
+
+	menu_item = gtk_menu_item_new_with_label(_("Install "
+	    "Updates\342\200\246"));
+	gtk_actionable_set_action_name(GTK_ACTIONABLE(menu_item),
+	    "app.install-updates");
+	gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+
+	menu_item = gtk_menu_item_new_with_label(_("About\342\200\246"));
+	gtk_actionable_set_action_name(GTK_ACTIONABLE(menu_item),
+	    "app.about");
+	gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+
+	gtk_widget_show_all(menu);
+	app_indicator_set_menu(self->indicator, GTK_MENU(menu));
+
+	update_ui(self);
+
+	/* keep GApplication running */
+	g_application_hold(application);
+
+	g_object_unref(settings_backend);
+	g_free(settings_filename);
+}
+
+static void
+pui_application_shutdown(GApplication *application)
+{
+	GApplicationClass *application_class =
+	    G_APPLICATION_CLASS(pui_application_parent_class);
+
+	application_class->shutdown(application);
+}
+
+static gint
+pui_application_handle_local_options(GApplication *application,
+    GVariantDict *options)
+{
+	gchar		*messages_debug;
+	gchar		**args = NULL;
+	GError		*error = NULL;
+
+	/* filename arguments are not allowed */
+	if (g_variant_dict_lookup(options, G_OPTION_REMAINING, "^a&ay",
+	    &args)) {
+		g_printerr("invalid argument: \"%s\"\n", args[0]);
+		g_free(args);
+		return (1);
+	}
+
+	if (g_variant_dict_contains(options, "version")) {
+		g_print("%s %s\n", PACKAGE, VERSION);
+
+		/* quit */
+		return (0);
+	}
+
+	if (g_variant_dict_contains(options, "debug")) {
+		/* enable debug logging */
+		messages_debug = g_strjoin(":", G_LOG_DOMAIN,
+		    g_getenv("G_MESSAGES_DEBUG"), NULL);
+		g_setenv("G_MESSAGES_DEBUG", messages_debug, TRUE);
+		g_free(messages_debug);
+	}
+
+	/*
+	 * register with the session bus so that it is possible to discern
+	 * between remote and primary instance and that remote actions can be
+	 * invoked, this causes the startup signal to be emitted which, in case
+	 * of the primary instance, starts to instantiate the
+	 * backend with the given values
+	 */
+	if (!g_application_register(application, NULL, &error)) {
+		g_critical("g_application_register: %s", error->message);
+		g_error_free(error);
+		return (1);
+	}
+
+	if (g_variant_dict_contains(options, "quit")) {
+		/* only valid if a remote instance is running */
+		if (!g_application_get_is_remote(application)) {
+			g_printerr("%s is not running\n", g_get_prgname());
+			return (1);
+		}
+
+		/* signal remote instance to quit */
+		g_action_group_activate_action(G_ACTION_GROUP(application),
+		    "quit", NULL);
+
+		/* quit local instance */
+		return (0);
+	}
+
+	/* proceed with default command line processing */
+	return (-1);
+}
+
+static void
+pui_application_activate(GApplication *application) {
+	GApplicationClass *application_class =
+	    G_APPLICATION_CLASS(pui_application_parent_class);
+
+	/* do nothing, implementation required by GApplication */
+
+	application_class->activate(application);
+}
+
+static void
+pui_application_set_property(GObject *object, guint property_id,
+    const GValue *value, GParamSpec *pspec)
+{
+	PuiApplication	*self = PUI_APPLICATION(object);
+
+	switch (property_id) {
+	case PROP_UPDATE_COMMAND:
+		g_free(self->update_command);
+		self->update_command = g_value_dup_string(value);
+		g_debug("property \"update-command\" set to \"%s\"",
+		    self->update_command);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+		break;
+	}
+}
+
+static void
+pui_application_get_property(GObject *object, guint property_id, GValue *value,
+    GParamSpec *pspec)
+{
+	PuiApplication	*self = PUI_APPLICATION(object);
+
+	switch (property_id) {
+	case PROP_UPDATE_COMMAND:
+		g_value_set_string(value, self->update_command);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+		break;
+	}
+}
+
+static void
+pui_application_dispose(GObject *object)
+{
+	PuiApplication	*self = PUI_APPLICATION(object);
+	gsize		i;
+
+	if (self->settings != NULL) {
+		g_signal_handlers_disconnect_by_data(self->settings, self);
+		g_clear_object(&self->settings);
+	}
+
+	if (self->cancellable != NULL) {
+		g_cancellable_cancel(self->cancellable);
+		g_clear_object(&self->cancellable);
+	}
+
+	if (self->backend != NULL) {
+		g_clear_object(&self->backend);
+	}
+
+	if (self->indicator != NULL) {
+		g_clear_object(&self->indicator);
+	}
+
+	if (self->about_dialog != NULL) {
+		g_clear_pointer(&self->about_dialog,
+		    (GDestroyNotify)(gtk_widget_destroy));
+	}
+
+	for (i = 0; i < G_N_ELEMENTS(self->icons); i++) {
+		if (self->icons[i] != NULL) {
+			g_clear_object(&self->icons[i]);
+		}
+	}
+
+	G_OBJECT_CLASS(pui_application_parent_class)->dispose(object);
+}
+
+static void
+pui_application_finalize(GObject *object)
+{
+	PuiApplication	*self = PUI_APPLICATION(object);
+
+	g_free(self->update_command);
+	g_free(self->error_message);
+
+	G_OBJECT_CLASS(pui_application_parent_class)->finalize(object);
+}
+
+static void
+pui_application_class_init(PuiApplicationClass *klass)
+{
+	GObjectClass	*object_class = G_OBJECT_CLASS(klass);
+	GApplicationClass *application_class = G_APPLICATION_CLASS(klass);
+
+	object_class->set_property = pui_application_set_property;
+	object_class->get_property = pui_application_get_property;
+	object_class->dispose = pui_application_dispose;
+	object_class->finalize = pui_application_finalize;
+
+	properties[PROP_UPDATE_COMMAND] = g_param_spec_string("update-command",
+	    "Update command", "Command for installing updates", NULL,
+	    G_PARAM_READWRITE);
+
+	g_object_class_install_properties(object_class, PROP_LAST, properties);
+
+	application_class->startup = pui_application_startup;
+	application_class->shutdown = pui_application_shutdown;
+	application_class->handle_local_options =
+	    pui_application_handle_local_options;
+	application_class->activate = pui_application_activate;
+}
+
+static void
+pui_application_init(PuiApplication *self)
+{
+	g_application_add_main_option_entries(G_APPLICATION(self),
+	    cmd_options);
+
+	self->cancellable = g_cancellable_new();
+}
+
+PuiApplication *
+pui_application_new(void)
+{
+	return (g_object_new(PUI_TYPE_APPLICATION, "application-id",
+	    APPLICATION_ID, NULL));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pui-application.h	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	PUI_APPLICATION_H
+#define	PUI_APPLICATION_H
+
+#include <gio/gio.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define	PUI_TYPE_APPLICATION	(pui_application_get_type())
+
+G_DECLARE_FINAL_TYPE(PuiApplication, pui_application, PUI, APPLICATION,
+    GApplication)
+
+PuiApplication *	pui_application_new(void);
+
+G_END_DECLS
+
+#endif /* !PUI_APPLICATION_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pui-backend.c	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,464 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <packagekit-glib2/packagekit.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <utime.h>
+
+#include "pui-common.h"
+#include "pui-backend.h"
+#include "pui-get-updates.h"
+
+struct _PuiBackend {
+	GObject		parent_instance;
+	PkControl	*pk_control;
+	GCancellable	*cancellable;
+	gint64		last_check;
+	PkNetworkEnum	network_state;
+	guint		periodic_check_id;
+	guint		refresh_interval;
+	guint		important_updates;
+	guint		normal_updates;
+};
+
+static void	pui_backend_async_initable_iface_init(gpointer, gpointer);
+
+G_DEFINE_TYPE_WITH_CODE(PuiBackend, pui_backend, G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE(G_TYPE_ASYNC_INITABLE,
+    pui_backend_async_initable_iface_init))
+
+enum {
+    STATE_CHANGED,
+    RESTART_REQUIRED,
+    SIGNAL_LAST
+};
+
+enum {
+    PROP_0,
+    PROP_IMPORTANT_UPDATES,
+    PROP_NORMAL_UPDATES,
+    PROP_REFRESH_INTERVAL,
+    PROP_LAST
+};
+
+static guint	signals[SIGNAL_LAST] = { 0 };
+static GParamSpec *properties[PROP_LAST] = { NULL };
+
+static gboolean	periodic_check(gpointer);
+
+GQuark
+pui_backend_error_quark(void)
+{
+	return (g_quark_from_static_string("pui-backend-error-quark"));
+}
+
+static void
+process_pk_package(gpointer data, gpointer user_data)
+{
+	PkPackage	*package = data;
+	PuiBackend	*self = user_data;
+	PkInfoEnum	type_info = pk_package_get_info(package);
+
+	switch (type_info) {
+	case PK_INFO_ENUM_LOW:		/* FALLTHROUGH */
+	case PK_INFO_ENUM_ENHANCEMENT:	/* FALLTHROUGH */
+	case PK_INFO_ENUM_NORMAL:
+		self->normal_updates++;
+		break;
+	case PK_INFO_ENUM_BUGFIX:	/* FALLTHROUGH */
+	case PK_INFO_ENUM_IMPORTANT:	/* FALLTHROUGH */
+	case PK_INFO_ENUM_SECURITY:
+		self->important_updates++;
+		break;
+	default:
+		break;
+	}
+}
+
+static void
+on_get_updates_finished(GObject *source_object, GAsyncResult *async_result,
+    gpointer user_data)
+{
+	PuiBackend	*self = user_data;
+	GPtrArray	*package_list = NULL;
+	GError		*error = NULL;
+	guint		prev_normal_updates = self->normal_updates;
+	guint		prev_important_updates = self->important_updates;
+
+	package_list = pui_get_updates_finish(async_result, &error);
+	if (package_list == NULL) {
+		if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
+		    g_error_matches(error, PUI_GET_UPDATES_ERROR,
+		    PUI_GET_UPDATES_ERROR_CANCELLED)) {
+			/* cancelled */
+			g_debug("cancelled checking for updates");
+		} else {
+			g_warning("failed to check for updates: %s",
+			    error->message);
+		}
+		g_error_free(error);
+		goto out;
+	}
+
+	self->normal_updates = 0;
+	self->important_updates = 0;
+	g_ptr_array_foreach(package_list, process_pk_package, self);
+	g_debug("normal updates: %u, important updates: %u",
+	    self->normal_updates, self->important_updates);
+	if (self->normal_updates != prev_normal_updates) {
+		g_object_notify_by_pspec(G_OBJECT(self),
+		    properties[PROP_NORMAL_UPDATES]);
+	}
+	if (self->important_updates != prev_important_updates) {
+		g_object_notify_by_pspec(G_OBJECT(self),
+		    properties[PROP_IMPORTANT_UPDATES]);
+	}
+	if ((self->normal_updates != prev_normal_updates) ||
+	    (self->important_updates != prev_important_updates)) {
+		g_debug("emitting signal state-changed");
+		g_signal_emit(self, signals[STATE_CHANGED], 0);
+	}
+
+	/* last successful check */
+	self->last_check = g_get_monotonic_time();
+
+out:
+	/* reschedule periodic check */
+	if (self->network_state != PK_NETWORK_ENUM_OFFLINE) {
+		self->periodic_check_id =
+		    g_timeout_add_seconds(PUI_CHECK_UPDATES_INTERVAL,
+		    periodic_check, self);
+	}
+
+	if (package_list != NULL) {
+		g_ptr_array_unref(package_list);
+	}
+}
+
+static gboolean
+periodic_check(gpointer user_data)
+{
+	PuiBackend	*self = user_data;
+
+	g_debug("running periodic check");
+
+	pui_get_updates_async(self->pk_control, self->refresh_interval,
+	    self->cancellable, on_get_updates_finished, self);
+
+	/* next periodic check will be scheduled after completion */
+	self->periodic_check_id = 0;
+
+	return (G_SOURCE_REMOVE);
+}
+
+static void
+pui_backend_set_property(GObject *object, guint property_id,
+    const GValue *value, GParamSpec *pspec)
+{
+	PuiBackend	*self = PUI_BACKEND(object);
+
+	switch (property_id) {
+	case PROP_REFRESH_INTERVAL:
+		self->refresh_interval = g_value_get_uint(value);
+		g_debug("property \"refresh-interval\" set to %u",
+		    self->refresh_interval);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+		break;
+	}
+}
+
+static void
+pui_backend_get_property(GObject *object, guint property_id, GValue *value,
+    GParamSpec *pspec)
+{
+	PuiBackend	*self = PUI_BACKEND(object);
+
+	switch (property_id) {
+	case PROP_IMPORTANT_UPDATES:
+		g_value_set_uint(value, self->important_updates);
+		break;
+	case PROP_NORMAL_UPDATES:
+		g_value_set_uint(value, self->normal_updates);
+		break;
+	case PROP_REFRESH_INTERVAL:
+		g_value_set_uint(value, self->refresh_interval);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+		break;
+	}
+}
+
+static void
+pui_backend_dispose(GObject *object)
+{
+	PuiBackend	*self = PUI_BACKEND(object);
+
+	if (self->periodic_check_id != 0) {
+		g_source_remove(self->periodic_check_id);
+		self->periodic_check_id = 0;
+	}
+
+	if (self->cancellable != NULL) {
+		g_cancellable_cancel(self->cancellable);
+		g_clear_object(&self->cancellable);
+	}
+
+	if (self->pk_control != NULL) {
+		g_clear_object(&self->pk_control);
+	}
+
+	G_OBJECT_CLASS(pui_backend_parent_class)->dispose(object);
+}
+
+static void
+pui_backend_class_init(PuiBackendClass *klass)
+{
+	GObjectClass	*object_class = G_OBJECT_CLASS(klass);
+
+	object_class->set_property = pui_backend_set_property;
+	object_class->get_property = pui_backend_get_property;
+	object_class->dispose = pui_backend_dispose;
+
+	properties[PROP_IMPORTANT_UPDATES] =
+	    g_param_spec_uint("important-updates", "Important updates",
+	    "Number of available important updates", 0, G_MAXUINT, 0,
+	    G_PARAM_READABLE);
+
+	properties[PROP_NORMAL_UPDATES] =
+	    g_param_spec_uint("normal-updates", "Normal updates",
+	    "Number of available normal updates", 0, G_MAXUINT, 0,
+	    G_PARAM_READABLE);
+
+	properties[PROP_REFRESH_INTERVAL] =
+	    g_param_spec_uint("refresh-interval", "Refresh interval",
+	    "Interval in seconds for refreshing the package cache", 0,
+	    G_MAXUINT, PUI_DEFAULT_REFRESH_INTERVAL, G_PARAM_READWRITE);
+
+	g_object_class_install_properties(object_class, PROP_LAST, properties);
+
+	signals[STATE_CHANGED] = g_signal_new("state-changed",
+	    G_TYPE_FROM_CLASS(object_class),
+	    G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0,
+	    NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+	signals[RESTART_REQUIRED] = g_signal_new("restart-required",
+	    G_TYPE_FROM_CLASS(object_class),
+	    G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0,
+	    NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+static void
+pui_backend_init(PuiBackend *self)
+{
+	self->cancellable = g_cancellable_new();
+	self->pk_control = pk_control_new();
+}
+
+static void
+on_get_properties_finished(GObject *object, GAsyncResult *result,
+    gpointer user_data)
+{
+	PkControl	*control = PK_CONTROL(object);
+	PuiBackend	*self;
+	GTask		*task = user_data;
+	GError		*error = NULL;
+	gchar		*backend_name = NULL;
+	PkBitfield	roles = 0;
+	gchar		*roles_str = NULL;
+
+	self = g_task_get_task_data(task);
+
+	if (!pk_control_get_properties_finish(control, result, &error)) {
+		g_task_return_error(task, error);
+		goto out;
+	}
+
+	/* check whether the backend supports GetUpdates */
+	g_object_get(control, "backend-name", &backend_name, "roles", &roles,
+	    "network-state", &self->network_state, NULL);
+	g_debug("backend: %s", backend_name);
+	roles_str = pk_role_bitfield_to_string(roles);
+	g_debug("roles: %s", roles_str);
+	g_debug("network-state: %s",
+	    pk_network_enum_to_string(self->network_state));
+	if (!pk_bitfield_contain(roles, PK_ROLE_ENUM_GET_UPDATES)) {
+		error = g_error_new(PUI_BACKEND_ERROR,
+		    PUI_BACKEND_ERROR_GET_UPDATES_NOT_IMPLEMENTED,
+		    "Getting updates is not implemented in the %s PackageKit "
+		    "backend", backend_name);
+		g_task_return_error(task, error);
+		goto out;
+	}
+
+	g_task_return_boolean(task, TRUE);
+out:
+	g_free(roles_str);
+	g_free(backend_name);
+	g_object_unref(task);
+}
+
+static void
+on_notify_network_state(PkControl *pk_control, GParamSpec *pspec,
+    gpointer user_data)
+{
+	PuiBackend	*self = user_data;
+	PkNetworkEnum	network_state;
+	guint		elapsed_time;
+	guint		remaining_time;
+
+	g_object_get(pk_control, "network-state", &network_state, NULL);
+	g_debug("network state changed: %s",
+	    pk_network_enum_to_string(network_state));
+	if ((self->network_state == PK_NETWORK_ENUM_OFFLINE) &&
+	    (network_state != PK_NETWORK_ENUM_OFFLINE)) {
+		/* schedule periodic checks when coming back online */
+		elapsed_time = (g_get_monotonic_time() - self->last_check) /
+		    G_USEC_PER_SEC;
+		/*
+		 * if more time that the check interval has passed since the
+		 * last check, schedule a check after a short delay, otherwise
+		 * wait until the interval has passed
+		 */
+		remaining_time = (elapsed_time < PUI_CHECK_UPDATES_INTERVAL) ?
+		    PUI_CHECK_UPDATES_INTERVAL - elapsed_time :
+		    PUI_STARTUP_DELAY;
+		self->periodic_check_id = g_timeout_add_seconds(remaining_time,
+		    periodic_check, self);
+	} else if ((self->network_state != PK_NETWORK_ENUM_OFFLINE) &&
+	    (network_state == PK_NETWORK_ENUM_OFFLINE)) {
+		/* cancel periodic checks while offline */
+		if (self->periodic_check_id != 0) {
+			g_source_remove(self->periodic_check_id);
+		}
+	}
+	self->network_state = network_state;
+}
+
+static void
+on_updates_changed(PkControl *control, gpointer user_data)
+{
+	PuiBackend	*self = user_data;
+
+	/*
+	 * schedule a check after a short delay so that a rapid succession of
+	 * signals is coalesced
+	 */
+	if (self->network_state != PK_NETWORK_ENUM_OFFLINE) {
+		if (self->periodic_check_id != 0) {
+			g_source_remove(self->periodic_check_id);
+		}
+		self->periodic_check_id =
+		    g_timeout_add_seconds(PUI_STARTUP_DELAY, periodic_check,
+		    self);
+	}
+}
+
+static void
+on_restart_schedule(PkControl *control, gpointer user_data)
+{
+	PuiBackend	*self = user_data;
+
+	g_debug("emitting signal restart-required");
+	g_signal_emit(self, signals[RESTART_REQUIRED], 0);
+}
+
+static void
+pui_backend_init_async(GAsyncInitable *initable, int io_priority,
+    GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data)
+{
+	PuiBackend	*self = PUI_BACKEND(initable);
+	GTask		*task;
+
+	task = g_task_new(G_OBJECT(initable), cancellable, callback, user_data);
+	g_task_set_priority(task, io_priority);
+	g_task_set_task_data(task, g_object_ref(self),
+	    (GDestroyNotify)g_object_unref);
+
+	pk_control_get_properties_async(self->pk_control, cancellable,
+	    on_get_properties_finished, task);
+}
+
+static gboolean
+pui_backend_init_finish(GAsyncInitable *initable, GAsyncResult *result,
+    GError **errorp)
+{
+	PuiBackend	*self = PUI_BACKEND(initable);
+	GTask		*task = G_TASK(result);
+
+	if (!g_task_propagate_boolean(task, errorp)) {
+		return (FALSE);
+	}
+
+	/* get notification when the network state changes */
+	g_signal_connect(self->pk_control, "notify::network-state",
+	    G_CALLBACK(on_notify_network_state), self);
+	/* get notifications when the available package updates change */
+	g_signal_connect(self->pk_control, "updates-changed",
+	    G_CALLBACK(on_updates_changed), self);
+	/* get notifications when an application restart is required */
+	g_signal_connect(self->pk_control, "restart-schedule",
+	    G_CALLBACK(on_restart_schedule), self);
+	/* schedule first check after a small delay */
+	self->periodic_check_id = g_timeout_add_seconds(PUI_STARTUP_DELAY,
+	    periodic_check, self);
+
+	return (TRUE);
+}
+
+static void
+pui_backend_async_initable_iface_init(gpointer g_iface, gpointer iface_data)
+{
+	GAsyncInitableIface	*iface = g_iface;
+
+	iface->init_async = pui_backend_init_async;
+	iface->init_finish = pui_backend_init_finish;
+}
+
+void
+pui_backend_new_async(GCancellable *cancellable, GAsyncReadyCallback callback,
+    gpointer user_data)
+{
+	g_async_initable_new_async(PUI_TYPE_BACKEND, G_PRIORITY_DEFAULT,
+	    cancellable, callback, user_data, NULL);
+}
+
+PuiBackend *
+pui_backend_new_finish(GAsyncResult *result, GError **errorp)
+{
+	GObject	*object;
+	GObject	*source_object;
+
+	source_object = g_async_result_get_source_object(result);
+	object = g_async_initable_new_finish(G_ASYNC_INITABLE(source_object),
+	    result, errorp);
+	g_object_unref(source_object);
+
+	return ((object != NULL) ? PUI_BACKEND(object) : NULL);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pui-backend.h	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	PUI_BACKEND_H
+#define	PUI_BACKEND_H
+
+#include <gio/gio.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define	PUI_TYPE_BACKEND	(pui_backend_get_type())
+
+#define	PUI_BACKEND_ERROR	(pui_backend_error_quark())
+
+G_DECLARE_FINAL_TYPE(PuiBackend, pui_backend, PUI, BACKEND, GObject)
+
+enum {
+    PUI_BACKEND_ERROR_GET_UPDATES_NOT_IMPLEMENTED
+};
+
+GQuark		pui_backend_error_quark(void);
+void		pui_backend_new_async(GCancellable *, GAsyncReadyCallback,
+    gpointer);
+PuiBackend *	pui_backend_new_finish(GAsyncResult *, GError **);
+
+G_END_DECLS
+
+#endif /* !PUI_BACKEND_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pui-common.h	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	PUI_COMMON_H
+#define	PUI_COMMON_H
+
+G_BEGIN_DECLS
+
+#ifndef	PUI_STARTUP_DELAY
+#define	PUI_STARTUP_DELAY		(3 * 60)
+#endif /* !PUI_STARTUP_DELAY */
+
+#ifndef	PUI_CHECK_UPDATES_INTERVAL
+#define	PUI_CHECK_UPDATES_INTERVAL	(60 * 60)
+#endif /* !PUI_CHECK_UPDATES_INTERVAL */
+
+#ifndef	PUI_DEFAULT_REFRESH_INTERVAL
+#define	PUI_DEFAULT_REFRESH_INTERVAL	(24 * 60 * 60)
+#endif /* !PUI_DEFAULT_REFRESH_INTERVAL */
+
+G_END_DECLS
+
+#endif /* !PUI_COMMON_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pui-get-updates.c	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pui-get-updates.h"
+
+typedef struct {
+	PkControl	*pk_control;
+	PkTask		*pk_task;
+	guint		refresh_interval;
+} PuiGetUpdates;
+
+GQuark
+pui_get_updates_error_quark(void)
+{
+	return (g_quark_from_static_string("pui-get-updates-error-quark"));
+}
+
+static void
+on_get_updates_finished(GObject *source_object, GAsyncResult *async_result,
+    gpointer user_data)
+{
+	GTask		*task = user_data;
+	PuiGetUpdates *get_updates;
+	PkResults	*results = NULL;
+	PkError		*pk_error = NULL;
+	GError		*error = NULL;
+	gint		error_code;
+	GPtrArray	*package_list;
+
+	get_updates = g_task_get_task_data(task);
+
+	g_debug("get updates transaction finished");
+	results = pk_client_generic_finish(PK_CLIENT(get_updates->pk_task),
+	    async_result, &error);
+	if (results == NULL) {
+		/* pass the error on */
+		g_task_return_error(task, error);
+		goto out;
+	}
+
+	pk_error = pk_results_get_error_code(results);
+	if (pk_error != NULL) {
+		/* transaction failed, return error */
+		g_debug("failed to refresh the cache: %s",
+		    pk_error_get_details(pk_error));
+		if (pk_error_get_code(pk_error) ==
+		    PK_ERROR_ENUM_TRANSACTION_CANCELLED) {
+			error_code = PUI_GET_UPDATES_ERROR_CANCELLED;
+		} else {
+			error_code =
+			    PUI_GET_UPDATES_ERROR_GET_UPDATES_FAILED;
+		}
+		error = g_error_new(PUI_GET_UPDATES_ERROR, error_code,
+		    "Failed to get package updates: %s",
+		    pk_error_get_details(pk_error));
+		g_task_return_error(task, error);
+		g_object_unref(pk_error);
+		goto out;
+	}
+
+	/* return results */
+	package_list = pk_results_get_package_array(results);
+	g_assert(package_list != NULL);
+	g_task_return_pointer(task, package_list,
+	    (GDestroyNotify)g_ptr_array_unref);
+
+out:
+	if (results != NULL) {
+		g_object_unref(results);
+	}
+	g_object_unref(task);
+}
+
+static void
+on_refresh_cache_finished(GObject *source_object, GAsyncResult *async_result,
+    gpointer user_data)
+{
+	GTask		*task = user_data;
+	PuiGetUpdates *get_updates;
+	PkResults	*results = NULL;
+	PkClient	*pk_client;
+	GError		*error = NULL;
+	PkError		*pk_error = NULL;
+	gint		error_code;
+
+	get_updates = g_task_get_task_data(task);
+	pk_client = PK_CLIENT(get_updates->pk_task);
+
+	g_debug("refresh cache transaction finished");
+	results = pk_client_generic_finish(pk_client, async_result, &error);
+	if (results == NULL) {
+		g_task_return_error(task, error);
+		goto out;
+	}
+
+	pk_error = pk_results_get_error_code(results);
+	if (pk_error != NULL) {
+		/* transaction failed, return error */
+		g_debug("failed to refresh the cache: %s",
+		    pk_error_get_details(pk_error));
+		if (pk_error_get_code(pk_error) ==
+		    PK_ERROR_ENUM_TRANSACTION_CANCELLED) {
+			error_code = PUI_GET_UPDATES_ERROR_CANCELLED;
+		} else {
+			error_code = PUI_GET_UPDATES_ERROR_REFRESH_FAILED;
+		}
+		error = g_error_new(PUI_GET_UPDATES_ERROR, error_code,
+		    "Failed to refresh the cache: %s",
+		    pk_error_get_details(pk_error));
+		g_task_return_error(task, error);
+		g_object_unref(pk_error);
+		goto out;
+	}
+
+	/* cache is up to date, get updates */
+	pk_client_get_updates_async(pk_client,
+	    pk_bitfield_value(PK_FILTER_ENUM_NONE),
+	    g_task_get_cancellable(task), NULL, NULL, on_get_updates_finished,
+	    task);
+
+out:
+	if (results != NULL) {
+		g_object_unref(results);
+	}
+}
+
+static void
+on_get_time_since_refresh_finished(GObject *source_object,
+    GAsyncResult *async_result, gpointer user_data)
+{
+	GTask		*task = user_data;
+	PuiGetUpdates *get_updates;
+	guint		last_refresh;
+	GError		*error = NULL;
+	PkClient	*pk_client;
+
+	get_updates = g_task_get_task_data(task);
+	pk_client = PK_CLIENT(get_updates->pk_task);
+
+	last_refresh =
+	    pk_control_get_time_since_action_finish(get_updates->pk_control,
+	    async_result, &error);
+	if (last_refresh == 0) {
+		g_task_return_error(task, error);
+		g_object_unref(task);
+		return;
+	}
+	g_debug("time since last cache refresh: %us", last_refresh);
+
+	if (last_refresh > get_updates->refresh_interval) {
+		/* cache is out of date, refresh first */
+		g_debug("refreshing the cache");
+		pk_client_refresh_cache_async(pk_client, FALSE,
+		    g_task_get_cancellable(task), NULL, NULL,
+		    on_refresh_cache_finished, task);
+	} else {
+		/* cache is up to date, get updates */
+		g_debug("getting updates");
+		pk_client_get_updates_async(pk_client,
+		    pk_bitfield_value(PK_FILTER_ENUM_NONE),
+		    g_task_get_cancellable(task), NULL, NULL,
+		    on_get_updates_finished, task);
+	}
+}
+
+static void
+pui_get_updates_free(gpointer data)
+{
+	PuiGetUpdates *get_updates = data;
+
+	g_object_unref(get_updates->pk_control);
+	g_object_unref(get_updates->pk_task);
+	g_slice_free(PuiGetUpdates, data);
+}
+
+void
+pui_get_updates_async(PkControl *pk_control, guint refresh_interval,
+    GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data)
+{
+	PuiGetUpdates *get_updates;
+	GTask		*task;
+	PkClient	*pk_client;
+
+	get_updates = g_slice_new0(PuiGetUpdates);
+	get_updates->pk_control = g_object_ref(pk_control);
+	get_updates->pk_task = pk_task_new();
+	get_updates->refresh_interval = refresh_interval;
+
+	pk_client = PK_CLIENT(get_updates->pk_task);
+	pk_client_set_cache_age(pk_client, refresh_interval);
+	pk_client_set_background(pk_client, TRUE);
+
+	task = g_task_new(NULL, cancellable, callback, user_data);
+	g_task_set_task_data(task, get_updates, pui_get_updates_free);
+
+	/* check whether to refresh the cache before checking for updates */
+	g_debug("getting the time since the cache was last refreshed");
+	pk_control_get_time_since_action_async(pk_control,
+	    PK_ROLE_ENUM_REFRESH_CACHE, cancellable,
+	    on_get_time_since_refresh_finished, task);
+}
+
+GPtrArray *
+pui_get_updates_finish(GAsyncResult *result, GError **errorp)
+{
+	return (g_task_propagate_pointer(G_TASK(result), errorp));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pui-get-updates.h	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	PUI_GET_UPDATES_H
+#define	PUI_GET_UPDATES_H
+
+#include <gio/gio.h>
+#include <glib-object.h>
+#include <packagekit-glib2/packagekit.h>
+
+G_BEGIN_DECLS
+
+#define	PUI_GET_UPDATES_ERROR	(pui_get_updates_error_quark())
+
+enum {
+    PUI_GET_UPDATES_ERROR_REFRESH_FAILED,
+    PUI_GET_UPDATES_ERROR_GET_UPDATES_FAILED,
+    PUI_GET_UPDATES_ERROR_CANCELLED
+};
+
+GQuark		pui_get_updates_error_quark(void);
+void		pui_get_updates_async(PkControl *, guint, GCancellable *,
+    GAsyncReadyCallback, gpointer);
+GPtrArray *	pui_get_updates_finish(GAsyncResult *, GError **);
+
+G_END_DECLS
+
+#endif /* !PUI_GET_UPDATES_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pui-types.c	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "pui-types.h"
+
+#define	PUI_DEFINE_ENUM_VALUE(valuename, valuenick) \
+    { valuename, #valuename, #valuenick },
+#define PUI_DEFINE_ENUM_TYPE(EnumName, enum_name, values) \
+    GType                                                                     \
+    enum_name##_get_type(void)                                                \
+    {                                                                         \
+    	static volatile gsize g_define_type_id__volatile = 0;                 \
+    	static const GEnumValue v[] = {                                       \
+    	    values                                                            \
+    	    { 0 }                                                             \
+    	};                                                                    \
+    	GType		g_define_type_id;                                     \
+    	if (g_once_init_enter(&g_define_type_id__volatile)) {                 \
+    		g_define_type_id =                                            \
+    		    g_enum_register_static(g_intern_static_string(#EnumName), \
+    		    v);                                                       \
+    		g_once_init_leave(&g_define_type_id__volatile,                \
+    		    g_define_type_id);                                        \
+    	}                                                                     \
+    	return (g_define_type_id__volatile);                                  \
+    }
+
+PUI_DEFINE_ENUM_TYPE(PuiState, pui_state,
+    PUI_DEFINE_ENUM_VALUE(PUI_STATE_UP_TO_DATE, "up-to-date")
+    PUI_DEFINE_ENUM_VALUE(PUI_STATE_NORMAL_UPDATES_AVAILABLE,
+    "normal-updates-available")
+    PUI_DEFINE_ENUM_VALUE(PUI_STATE_IMPORTANT_UPDATES_AVAILABLE,
+    "important-updates-available")
+    PUI_DEFINE_ENUM_VALUE(PUI_STATE_ERROR, "error"))
+
+gchar *
+pui_types_enum_to_string(GType type, gint value)
+{
+	GTypeClass	*type_class;
+	GEnumValue	*enum_value;
+
+	type_class = g_type_class_ref(type);
+
+	g_return_val_if_fail(G_IS_ENUM_CLASS(type_class), NULL);
+
+	enum_value = g_enum_get_value(G_ENUM_CLASS(type_class), value);
+	if (enum_value == NULL) {
+		return (g_strdup_printf("%d", value));
+	}
+
+	return (g_strdup(enum_value->value_nick));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pui-types.h	Sun May 20 11:32:57 2018 +0200
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef	PUI_TYPES_H
+#define	PUI_TYPES_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define	PUI_TYPE_STATE	(pui_state_get_type())
+
+typedef enum {
+    PUI_STATE_INITIAL,
+    PUI_STATE_UP_TO_DATE,
+    PUI_STATE_NORMAL_UPDATES_AVAILABLE,
+    PUI_STATE_IMPORTANT_UPDATES_AVAILABLE,
+    PUI_STATE_ERROR,
+    PUI_STATE_LAST
+} PuiState;
+
+GType	pui_state_get_type(void);
+gchar *	pui_types_enum_to_string(GType, gint);
+
+G_END_DECLS
+
+#endif /* !PUI_TYPES_H */