[gnome-software/wip/william/cherry-pick-3] WIP



commit 57d09c134865f4c6d91addf309f59a4322b550a7
Author: William Hua <william hua canonical com>
Date:   Sat May 28 10:57:17 2016 -0400

    WIP

 configure.ac                                 |   28 +
 po/POTFILES.in                               |    3 +
 src/gnome-software.gresource.xml             |    2 +
 src/gs-app-list.c                            |   13 +-
 src/gs-app-row.c                             |    9 -
 src/gs-app.c                                 |    3 +
 src/gs-application.c                         |   84 ++-
 src/gs-common.c                              |    2 +-
 src/gs-dbus-helper.c                         |    9 +-
 src/gs-menus.ui                              |    2 +-
 src/gs-os-release.c                          |   21 +
 src/gs-os-release.h                          |    1 +
 src/gs-plugin.h                              |    1 +
 src/gs-review-dialog.c                       |   10 +-
 src/gs-shell-details.c                       |   21 +-
 src/gs-shell-search.c                        |    6 +
 src/gs-shell-updates.c                       |   14 +-
 src/gs-update-monitor.c                      |   29 +-
 src/gtk-style-hc.css                         |    8 +-
 src/gtk-style.css                            |    8 +-
 src/plugins/Makefile.am                      |   65 ++-
 src/plugins/com.canonical.Unity.Launcher.xml |   15 +
 src/plugins/gs-plugin-appstream.c            |   15 +
 src/plugins/gs-plugin-apt.cc                 | 1154 ++++++++++++++++++++++++++
 src/plugins/gs-plugin-packagekit-refine.c    |   15 -
 src/plugins/gs-plugin-packagekit-refresh.c   |    3 -
 src/plugins/gs-plugin-provenance.c           |    1 +
 src/plugins/gs-plugin-snappy.c               |  476 +++++++++++
 src/plugins/gs-plugin-ubuntu-reviews.c       |  421 +++++++++-
 src/plugins/gs-ubuntu-snapd.c                |  277 ++++++
 src/plugins/gs-ubuntu-snapd.h                |   41 +
 src/plugins/gs-ubuntuone-dialog.c            |  666 +++++++++++++++
 src/plugins/gs-ubuntuone-dialog.h            |   45 +
 src/plugins/gs-ubuntuone-dialog.ui           |  386 +++++++++
 src/plugins/gs-ubuntuone.c                   |  410 +++++++++
 src/plugins/gs-ubuntuone.h                   |   49 ++
 src/plugins/ubuntu-one.png                   |  Bin 0 -> 2540 bytes
 37 files changed, 4237 insertions(+), 76 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 3f89d83..bbe87aa 100644
--- a/configure.ac
+++ b/configure.ac
@@ -11,6 +11,7 @@ AC_CONFIG_MACRO_DIR([m4])
 m4_ifdef([AM_SILENT_RULES],[AM_SILENT_RULES([yes])])
 
 AC_PROG_CC
+AC_PROG_CXX
 AC_PROG_INSTALL
 LT_INIT
 AM_PROG_CC_C_O
@@ -68,6 +69,8 @@ PKG_CHECK_MODULES(SQLITE, sqlite3)
 PKG_CHECK_MODULES(SOUP, libsoup-2.4 >= 2.51.92)
 PKG_CHECK_MODULES(GSETTINGS_DESKTOP_SCHEMAS, gsettings-desktop-schemas >= 3.11.5)
 PKG_CHECK_MODULES(GNOME_DESKTOP, gnome-desktop-3.0 >= 3.17.92)
+PKG_CHECK_MODULES(OAUTH, oauth)
+PKG_CHECK_MODULES(LIBSECRET, libsecret-1)
 AC_PATH_PROG(APPSTREAM_UTIL, [appstream-util], [unfound])
 AC_ARG_ENABLE(man,
               [AS_HELP_STRING([--enable-man],
@@ -118,6 +121,30 @@ AS_IF([test "x$have_packagekit" = "xyes"], [
 ])
 AM_CONDITIONAL(HAVE_PACKAGEKIT, test "$have_packagekit" != no)
 
+# libapt
+AC_ARG_ENABLE(apt,
+              [AS_HELP_STRING([--enable-apt],
+                              [enable apt support [default=auto]])],,
+              enable_apt=maybe)
+AS_IF([test "x$enable_apt" != "xno"], [
+       AC_LANG_PUSH([C++])
+       AC_CHECK_HEADERS([apt-pkg/init.h],
+                        [have_apt=yes],
+                        [have_apt=no])
+       AC_LANG_POP
+], [
+    have_apt=no
+])
+
+AS_IF([test "x$have_apt" = "xyes"], [
+    AC_DEFINE(HAVE_APT,1,[Build apt support])
+], [
+    AS_IF([test "x$enable_apt" = "xyes"], [
+          AC_MSG_ERROR([apt support requested but 'libapt-pkg' was not found])
+    ])
+])
+AM_CONDITIONAL(HAVE_APT, test "$have_apt" != no)
+
 # PolicyKit
 AC_ARG_ENABLE(polkit,
               [AS_HELP_STRING([--enable-polkit],
@@ -334,6 +361,7 @@ echo "
         Dogtail:                   ${enable_dogtail}
         Self tests:                ${enable_tests}
         PackageKit support:        ${have_packagekit}
+        APT support:               ${have_apt}
         PolicyKit support:         ${have_polkit}
         Firmware support:          ${have_firmware}
         Limba support:             ${have_limba}
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 5b7e6b9..7fc57f1 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -56,6 +56,9 @@ src/gs-upgrade-banner.c
 src/gs-common.c
 [type: gettext/glade]src/gs-menus.ui
 src/org.gnome.Software.desktop.in
+src/plugins/gs-ubuntuone-dialog.c
+[type: gettext/glade]src/plugins/gs-ubuntuone-dialog.ui
+src/plugins/gs-plugin-snappy.c
 src/plugins/menu-spec-common.c
 [type: gettext/glade]src/gs-popular-tile.ui
 src/gs-shell-loading.c
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index 773ce8e..393c255 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -34,5 +34,7 @@
   <file preprocess="xml-stripblanks">org.freedesktop.PackageKit.xml</file>
   <file>gtk-style.css</file>
   <file>gtk-style-hc.css</file>
+  <file>plugins/ubuntu-one.png</file>
+  <file preprocess="xml-stripblanks">plugins/gs-ubuntuone-dialog.ui</file>
  </gresource>
 </gresources>
diff --git a/src/gs-app-list.c b/src/gs-app-list.c
index f0858c1..075ff82 100644
--- a/src/gs-app-list.c
+++ b/src/gs-app-list.c
@@ -258,9 +258,10 @@ gs_app_list_filter_duplicates (GsAppList *list)
 {
        guint i;
        GsApp *app;
-       GsApp *found;
-       const gchar *id;
+       GsApp *found, *found_source;
+       const gchar *id, *source;
        g_autoptr(GHashTable) hash = NULL;
+       g_autoptr(GHashTable) source_hash = NULL;
        g_autoptr(GsAppList) old = NULL;
 
        g_return_if_fail (GS_IS_APP_LIST (list));
@@ -271,18 +272,24 @@ gs_app_list_filter_duplicates (GsAppList *list)
 
        /* create a new list with just the unique items */
        hash = g_hash_table_new (g_str_hash, g_str_equal);
+       source_hash = g_hash_table_new (g_str_hash, g_str_equal);
        for (i = 0; i < old->array->len; i++) {
                app = gs_app_list_index (old, i);
                id = gs_app_get_id (app);
+               source = gs_app_get_source_default (app);
                if (id == NULL) {
                        gs_app_list_add (list, app);
                        continue;
                }
                found = g_hash_table_lookup (hash, id);
-               if (found == NULL) {
+               found_source = source != NULL ? g_hash_table_lookup (source_hash, source) : NULL;
+               if (found == NULL && found_source == NULL) {
                        gs_app_list_add (list, app);
                        g_hash_table_insert (hash, (gpointer) id,
                                             GUINT_TO_POINTER (1));
+                       if (source != NULL)
+                               g_hash_table_insert (source_hash, (gpointer) source,
+                                                    GUINT_TO_POINTER (1));
                        continue;
                }
                g_debug ("ignoring duplicate %s", id);
diff --git a/src/gs-app-row.c b/src/gs-app-row.c
index 5f634c5..d04b9a9 100644
--- a/src/gs-app-row.c
+++ b/src/gs-app-row.c
@@ -84,15 +84,6 @@ gs_app_row_get_description (GsAppRow *app_row)
        GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row);
        const gchar *tmp = NULL;
 
-       /* convert the markdown update description into PangoMarkup */
-       if (priv->show_update &&
-           (gs_app_get_state (priv->app) == AS_APP_STATE_UPDATABLE ||
-            gs_app_get_state (priv->app) == AS_APP_STATE_UPDATABLE_LIVE)) {
-               tmp = gs_app_get_update_details (priv->app);
-               if (tmp != NULL && tmp[0] != '\0')
-                       return g_string_new (tmp);
-       }
-
        if (gs_app_get_state (priv->app) == AS_APP_STATE_UNAVAILABLE)
                return g_string_new (gs_app_get_summary_missing (priv->app));
 
diff --git a/src/gs-app.c b/src/gs-app.c
index 323fde1..1bfa7f1 100644
--- a/src/gs-app.c
+++ b/src/gs-app.c
@@ -547,6 +547,9 @@ gs_app_set_state_internal (GsApp *app, AsAppState state)
        case AS_APP_STATE_INSTALLED:
                /* installed has to go into an action state */
                if (state == AS_APP_STATE_UNKNOWN ||
+                   state == AS_APP_STATE_INSTALLING ||
+                   state == AS_APP_STATE_UPDATABLE ||
+                   state == AS_APP_STATE_UPDATABLE_LIVE ||
                    state == AS_APP_STATE_REMOVING)
                        state_change_ok = TRUE;
                break;
diff --git a/src/gs-application.c b/src/gs-application.c
index a68947c..065298b 100644
--- a/src/gs-application.c
+++ b/src/gs-application.c
@@ -247,15 +247,97 @@ gs_application_dbus_unregister (GApplication    *application,
 }
 
 static void
+refreshed_cb (GObject      *source_object,
+             GAsyncResult *res,
+             gpointer      user_data)
+{
+       GsPluginLoader *loader = GS_PLUGIN_LOADER (source_object);
+
+       if (gs_plugin_loader_refresh_finish (loader, res, NULL)) {
+               gs_plugin_loader_refresh_async (loader,
+                                               0,
+                                               GS_PLUGIN_REFRESH_FLAGS_UI,
+                                               NULL,
+                                               NULL,
+                                               NULL);
+       }
+}
+
+static void
+start_refresh (GsApplication *app)
+{
+       g_action_group_activate_action (G_ACTION_GROUP (app),
+                                       "set-mode",
+                                       g_variant_new_string ("updates"));
+
+       gs_plugin_loader_refresh_async (gs_application_get_plugin_loader (app),
+                                       0,
+                                       GS_PLUGIN_REFRESH_FLAGS_METADATA |
+                                       GS_PLUGIN_REFRESH_FLAGS_PAYLOAD |
+                                       GS_PLUGIN_REFRESH_FLAGS_UI,
+                                       NULL,
+                                       refreshed_cb,
+                                       app);
+}
+
+#define APP_INFO_PATH "/var/lib/app-info"
+
+static gboolean
+needs_refresh (void)
+{
+       g_autoptr(GDir) dir = g_dir_open (APP_INFO_PATH, 0, NULL);
+
+       return dir == NULL || g_dir_read_name (dir) == NULL;
+}
+
+static gboolean
+ask_refresh (gpointer user_data)
+{
+       GsApplication *app = user_data;
+       GtkWindow *parent;
+       GtkWidget *dialog;
+
+       parent = gtk_application_get_active_window (GTK_APPLICATION (app));
+
+       if (gtk_widget_is_visible (GTK_WIDGET (parent))) {
+               dialog = gtk_message_dialog_new (parent,
+                                                GTK_DIALOG_MODAL |
+                                                GTK_DIALOG_DESTROY_WITH_PARENT,
+                                                GTK_MESSAGE_QUESTION,
+                                                GTK_BUTTONS_YES_NO,
+                                                _("An update is needed to show all installable apps. 
Download now?"));
+
+               if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_YES)
+                       start_refresh (app);
+
+               gtk_widget_destroy (dialog);
+       }
+
+       return G_SOURCE_REMOVE;
+}
+
+static void
+first_run_dialog_destroyed_cb (GtkWidget *object,
+                              gpointer   user_data)
+{
+       g_signal_handlers_disconnect_by_data (object, user_data);
+
+       if (needs_refresh ())
+               gdk_threads_add_idle (ask_refresh, user_data);
+}
+
+static void
 gs_application_show_first_run_dialog (GsApplication *app)
 {
        GtkWidget *dialog;
 
        if (g_settings_get_boolean (app->settings, "first-run") == TRUE) {
                dialog = gs_first_run_dialog_new ();
+               g_signal_connect (dialog, "destroy", G_CALLBACK (first_run_dialog_destroyed_cb), app);
                gs_shell_modal_dialog_present (app->shell, GTK_DIALOG (dialog));
                g_settings_set_boolean (app->settings, "first-run", FALSE);
-       }
+       } else if (needs_refresh ())
+               gdk_threads_add_idle (ask_refresh, app);
 }
 
 static void
diff --git a/src/gs-common.c b/src/gs-common.c
index 29a6673..92f2f0a 100644
--- a/src/gs-common.c
+++ b/src/gs-common.c
@@ -136,7 +136,7 @@ gs_app_notify_installed (GsApp *app)
         * has been successfully installed */
        summary = g_strdup_printf (_("%s is now installed"), gs_app_get_name (app));
        n = g_notification_new (summary);
-       if (gs_app_get_kind (app) == AS_APP_KIND_DESKTOP) {
+       if (gs_app_get_kind (app) == AS_APP_KIND_DESKTOP && !gs_utils_is_current_desktop ("Unity")) {
                /* TRANSLATORS: this is button that opens the newly installed application */
                g_notification_add_button_with_target (n, _("Launch"),
                                                       "app.launch", "s",
diff --git a/src/gs-dbus-helper.c b/src/gs-dbus-helper.c
index 59a2c4f..98dff67 100644
--- a/src/gs-dbus-helper.c
+++ b/src/gs-dbus-helper.c
@@ -32,6 +32,7 @@
 #include "gs-packagekit-modify2-generated.h"
 #include "gs-resources.h"
 #include "gs-shell-extras.h"
+#include "gs-common.h"
 #include "gs-utils.h"
 
 struct _GsDbusHelper {
@@ -345,9 +346,11 @@ notify_search_resources (GsShellExtrasMode   mode,
 
        n = g_notification_new (title);
        g_notification_set_body (n, body);
-       /* TRANSLATORS: this is a button that launches gnome-software */
-       g_notification_add_button_with_target (n, _("Find in Software"), "app.install-resources", "(s^ass)", 
mode_string, resources, "");
-       g_notification_set_default_action_and_target (n, "app.install-resources", "(s^ass)", mode_string, 
resources, "");
+       if (!gs_utils_is_current_desktop ("Unity")) {
+               /* TRANSLATORS: this is a button that launches gnome-software */
+               g_notification_add_button_with_target (n, _("Find in Software"), "app.install-resources", 
"(s^ass)", mode_string, resources, "");
+               g_notification_set_default_action_and_target (n, "app.install-resources", "(s^ass)", 
mode_string, resources, "");
+       }
        g_application_send_notification (g_application_get_default (), "install-resources", n);
 }
 
diff --git a/src/gs-menus.ui b/src/gs-menus.ui
index af7f63b..f6a68d9 100644
--- a/src/gs-menus.ui
+++ b/src/gs-menus.ui
@@ -4,7 +4,7 @@
   <menu id="app-menu">
     <section>
       <item>
-        <attribute name="label" translatable="yes">_Software Sources</attribute>
+        <attribute name="label" translatable="yes">_Software &amp; Updates</attribute>
         <attribute name="action">app.sources</attribute>
         <attribute name="hidden-when">action-disabled</attribute>
       </item>
diff --git a/src/gs-os-release.c b/src/gs-os-release.c
index 3398511..94e9bc4 100644
--- a/src/gs-os-release.c
+++ b/src/gs-os-release.c
@@ -45,6 +45,7 @@ struct _GsOsRelease
        gchar                   *id;
        gchar                   *version_id;
        gchar                   *pretty_name;
+       gchar                   *ubuntu_codename;
 };
 
 static void gs_os_release_initable_iface_init (GInitableIface *iface);
@@ -119,6 +120,10 @@ gs_os_release_initable_init (GInitable *initable,
                        os_release->pretty_name = g_strdup (tmp);
                        continue;
                }
+               if (g_strcmp0 (lines[i], "UBUNTU_CODENAME") == 0) {
+                       os_release->ubuntu_codename = g_strdup (tmp);
+                       continue;
+               }
        }
        return TRUE;
 }
@@ -199,6 +204,21 @@ gs_os_release_get_pretty_name (GsOsRelease *os_release)
 }
 
 /**
+ * gs_os_release_get_ubuntu_codename:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the Ubuntu code name from the os-release parser.
+ *
+ * Returns: a string, or %NULL
+ **/
+const gchar *
+gs_os_release_get_ubuntu_codename (GsOsRelease *os_release)
+{
+       g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+       return os_release->ubuntu_codename;
+}
+
+/**
  * gs_os_release_finalize:
  **/
 static void
@@ -210,6 +230,7 @@ gs_os_release_finalize (GObject *object)
        g_free (os_release->id);
        g_free (os_release->version_id);
        g_free (os_release->pretty_name);
+       g_free (os_release->ubuntu_codename);
        G_OBJECT_CLASS (gs_os_release_parent_class)->finalize (object);
 }
 
diff --git a/src/gs-os-release.h b/src/gs-os-release.h
index ffd338e..508b81f 100644
--- a/src/gs-os-release.h
+++ b/src/gs-os-release.h
@@ -39,6 +39,7 @@ const gchar   *gs_os_release_get_version      (GsOsRelease    *os_release);
 const gchar    *gs_os_release_get_id           (GsOsRelease    *os_release);
 const gchar    *gs_os_release_get_version_id   (GsOsRelease    *os_release);
 const gchar    *gs_os_release_get_pretty_name  (GsOsRelease    *os_release);
+const gchar    *gs_os_release_get_ubuntu_codename (GsOsRelease *os_release);
 
 G_END_DECLS
 
diff --git a/src/gs-plugin.h b/src/gs-plugin.h
index 230c4a1..a474569 100644
--- a/src/gs-plugin.h
+++ b/src/gs-plugin.h
@@ -195,6 +195,7 @@ typedef enum {
        GS_PLUGIN_REFRESH_FLAGS_METADATA                = 1 << 0,
        GS_PLUGIN_REFRESH_FLAGS_PAYLOAD                 = 1 << 1,
        GS_PLUGIN_REFRESH_FLAGS_INTERACTIVE             = 1 << 2,
+       GS_PLUGIN_REFRESH_FLAGS_UI                      = 1 << 3,
        /*< private >*/
        GS_PLUGIN_REFRESH_FLAGS_LAST
 } GsPluginRefreshFlags;
diff --git a/src/gs-review-dialog.c b/src/gs-review-dialog.c
index 9f41dc1..4a9a620 100644
--- a/src/gs-review-dialog.c
+++ b/src/gs-review-dialog.c
@@ -28,11 +28,11 @@
 #include "gs-review-dialog.h"
 #include "gs-star-widget.h"
 
-#define DESCRIPTION_LENGTH_MAX         3000    /* chars */
-#define DESCRIPTION_LENGTH_MIN         15      /* chars */
-#define SUMMARY_LENGTH_MAX             70      /* chars */
-#define SUMMARY_LENGTH_MIN             3       /* chars */
-#define WRITING_TIME_MIN               5       /* seconds */
+#define DESCRIPTION_LENGTH_MAX         10000   /* chars */
+#define DESCRIPTION_LENGTH_MIN         1       /* chars */
+#define SUMMARY_LENGTH_MAX             1000    /* chars */
+#define SUMMARY_LENGTH_MIN             1       /* chars */
+#define WRITING_TIME_MIN               0       /* seconds */
 
 struct _GsReviewDialog
 {
diff --git a/src/gs-shell-details.c b/src/gs-shell-details.c
index d14dd4c..9fe8c71 100644
--- a/src/gs-shell-details.c
+++ b/src/gs-shell-details.c
@@ -84,6 +84,7 @@ struct _GsShellDetails
        GtkWidget               *label_details_category_value;
        GtkWidget               *label_details_developer_title;
        GtkWidget               *label_details_developer_value;
+       GtkWidget               *label_details_license_title;
        GtkWidget               *label_details_license_value;
        GtkWidget               *label_details_origin_title;
        GtkWidget               *label_details_origin_value;
@@ -91,6 +92,7 @@ struct _GsShellDetails
        GtkWidget               *label_details_size_installed_value;
        GtkWidget               *label_details_size_download_title;
        GtkWidget               *label_details_size_download_value;
+       GtkWidget               *label_details_updated_title;
        GtkWidget               *label_details_updated_value;
        GtkWidget               *label_details_version_value;
        GtkWidget               *label_failed;
@@ -841,12 +843,16 @@ gs_shell_details_refresh_all (GsShellDetails *self)
                /* TRANSLATORS: this is where the license is not known */
                gtk_label_set_label (GTK_LABEL (self->label_details_license_value), C_("license", "Unknown"));
                gtk_widget_set_tooltip_text (self->label_details_license_value, NULL);
+               gtk_widget_set_visible (self->label_details_license_title, FALSE);
+               gtk_widget_set_visible (self->label_details_license_value, FALSE);
        } else {
                g_autofree gchar *license_markup = NULL;
                license_markup = gs_shell_details_get_license_markup (tmp);
                gtk_label_set_markup (GTK_LABEL (self->label_details_license_value),
                                      license_markup);
                gtk_widget_set_tooltip_text (self->label_details_license_value, NULL);
+               gtk_widget_set_visible (self->label_details_license_title, TRUE);
+               gtk_widget_set_visible (self->label_details_license_value, TRUE);
        }
 
        /* set version */
@@ -888,6 +894,8 @@ gs_shell_details_refresh_all (GsShellDetails *self)
            updated == GS_APP_INSTALL_DATE_UNSET) {
                /* TRANSLATORS: this is where the updated date is not known */
                gtk_label_set_label (GTK_LABEL (self->label_details_updated_value), C_("updated", "Never"));
+               gtk_widget_set_visible (self->label_details_updated_title, FALSE);
+               gtk_widget_set_visible (self->label_details_updated_value, FALSE);
        } else {
                g_autoptr(GDateTime) dt = NULL;
                g_autofree gchar *updated_str = NULL;
@@ -906,6 +914,12 @@ gs_shell_details_refresh_all (GsShellDetails *self)
                        gtk_label_set_markup (GTK_LABEL (self->label_details_updated_value), url->str);
                        g_string_free (url, TRUE);
                }
+
+               // Disabled on Ubuntu as we don't have history support
+               // gtk_widget_set_visible (self->label_details_updated_title, TRUE);
+               // gtk_widget_set_visible (self->label_details_updated_value, TRUE);
+               gtk_widget_set_visible (self->label_details_updated_title, FALSE);
+               gtk_widget_set_visible (self->label_details_updated_value, FALSE);
        }
 
        /* set the category */
@@ -1019,9 +1033,10 @@ gs_shell_details_refresh_all (GsShellDetails *self)
 
        /* hide the kudo details for non-desktop software */
        switch (gs_app_get_kind (self->app)) {
-       case AS_APP_KIND_DESKTOP:
+       // Hidden on Ubuntu since don't have appropriate information
+       /*case AS_APP_KIND_DESKTOP:
                gtk_widget_set_visible (self->grid_details_kudo, TRUE);
-               break;
+               break;*/
        default:
                gtk_widget_set_visible (self->grid_details_kudo, FALSE);
                break;
@@ -1814,6 +1829,7 @@ gs_shell_details_class_init (GsShellDetailsClass *klass)
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_category_value);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_developer_title);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_developer_value);
+       gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_license_title);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_license_value);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_origin_title);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_origin_value);
@@ -1821,6 +1837,7 @@ gs_shell_details_class_init (GsShellDetailsClass *klass)
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, 
label_details_size_download_value);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, 
label_details_size_installed_title);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, 
label_details_size_installed_value);
+       gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_updated_title);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_updated_value);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_version_value);
        gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_failed);
diff --git a/src/gs-shell-search.c b/src/gs-shell-search.c
index cb17cf8..60da255 100644
--- a/src/gs-shell-search.c
+++ b/src/gs-shell-search.c
@@ -258,6 +258,12 @@ gs_shell_search_get_app_sort_key (GsApp *app)
        /* sort installed, removing, other */
        key = g_string_sized_new (64);
 
+       /* sort snaps before other apps */
+       if (g_strcmp0 (gs_app_get_management_plugin (app), "snappy") == 0)
+               g_string_append (key, "9:");
+       else
+               g_string_append (key, "1:");
+
        /* sort missing codecs before applications */
        switch (gs_app_get_state (app)) {
        case AS_APP_STATE_UNAVAILABLE:
diff --git a/src/gs-shell-updates.c b/src/gs-shell-updates.c
index 62fa26a..3badb1d 100644
--- a/src/gs-shell-updates.c
+++ b/src/gs-shell-updates.c
@@ -486,7 +486,7 @@ gs_shell_updates_get_updates_cb (GsPluginLoader *plugin_loader,
        self->any_require_reboot = FALSE;
        for (i = 0; list != NULL && i < gs_app_list_length (list); i++) {
                GsApp *app = gs_app_list_index (list, i);
-               if (gs_app_get_state (app) != AS_APP_STATE_UPDATABLE_LIVE)
+               if (gs_app_get_state (app) == AS_APP_STATE_UPDATABLE)
                        self->all_updates_are_live = FALSE;
                if (gs_app_has_quirk (app, AS_APP_QUIRK_NEEDS_REBOOT))
                        self->any_require_reboot = TRUE;
@@ -998,11 +998,13 @@ gs_shell_updates_perform_update_cb (GsPluginLoader *plugin_loader,
                n = g_notification_new (_("Updates have been installed"));
                /* TRANSLATORS: the new apps will not be run until we restart */
                g_notification_set_body (n, _("A restart is required for them to take effect."));
-               /* TRANSLATORS: button text */
-               g_notification_add_button (n, _("Not Now"), "app.nop");
-               /* TRANSLATORS: button text */
-               g_notification_add_button_with_target (n, _("Restart"), "app.reboot", NULL);
-               g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+               if (!gs_utils_is_current_desktop ("Unity")) {
+                       /* TRANSLATORS: button text */
+                       g_notification_add_button (n, _("Not Now"), "app.nop");
+                       /* TRANSLATORS: button text */
+                       g_notification_add_button_with_target (n, _("Restart"), "app.reboot", NULL);
+                       g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+               }
                g_application_send_notification (g_application_get_default (), "restart-required", n);
        }
 }
diff --git a/src/gs-update-monitor.c b/src/gs-update-monitor.c
index 3b884e1..40d6fa1 100644
--- a/src/gs-update-monitor.c
+++ b/src/gs-update-monitor.c
@@ -91,17 +91,21 @@ notify_offline_update_available (GsUpdateMonitor *monitor)
                body = _("It is recommended that you install important updates now");
                n = g_notification_new (title);
                g_notification_set_body (n, body);
-               g_notification_add_button (n, _("Restart & Install"), "app.reboot-and-install");
-               g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+               if (!gs_utils_is_current_desktop ("Unity")) {
+                       g_notification_add_button (n, _("Restart & Install"), "app.reboot-and-install");
+                       g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+               }
                g_application_send_notification (monitor->application, "updates-available", n);
        } else {
                title = _("Software Updates Available");
                body = _("Important OS and application updates are ready to be installed");
                n = g_notification_new (title);
                g_notification_set_body (n, body);
-               g_notification_add_button (n, _("Not Now"), "app.nop");
-               g_notification_add_button_with_target (n, _("View"), "app.set-mode", "s", "updates");
-               g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+               if (!gs_utils_is_current_desktop ("Unity")) {
+                       g_notification_add_button (n, _("Not Now"), "app.nop");
+                       g_notification_add_button_with_target (n, _("View"), "app.set-mode", "s", "updates");
+                       g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+               }
                g_application_send_notification (monitor->application, "updates-available", n);
        }
 }
@@ -240,7 +244,8 @@ get_upgrades_finished_cb (GObject *object,
        /* TRANSLATORS: this is a distro upgrade */
        n = g_notification_new (_("Software Upgrade Available"));
        g_notification_set_body (n, body);
-       g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+       if (!gs_utils_is_current_desktop ("Unity"))
+               g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
        g_application_send_notification (monitor->application, "upgrades-available", n);
 }
 
@@ -428,8 +433,10 @@ get_updates_historical_cb (GObject *object, GAsyncResult *res, gpointer data)
                notification = g_notification_new (_("Software Updates Failed"));
                /* TRANSLATORS: message when we offline updates have failed */
                g_notification_set_body (notification, _("An important OS update failed to be installed."));
-               g_notification_add_button (notification, _("Show Details"), "app.show-offline-update-error");
-               g_notification_set_default_action (notification, "app.show-offline-update-error");
+               if (!gs_utils_is_current_desktop ("Unity")) {
+                       g_notification_add_button (notification, _("Show Details"), 
"app.show-offline-update-error");
+                       g_notification_set_default_action (notification, "app.show-offline-update-error");
+               }
                g_application_send_notification (monitor->application, "offline-updates", notification);
                return;
        }
@@ -465,8 +472,10 @@ get_updates_historical_cb (GObject *object, GAsyncResult *res, gpointer data)
         * users can't express their opinions here. In some languages
         * "Review (evaluate) something" is a different translation than
         * "Review (browse) something." */
-       g_notification_add_button_with_target (notification, C_("updates", "Review"), "app.set-mode", "s", 
"updated");
-       g_notification_set_default_action_and_target (notification, "app.set-mode", "s", "updated");
+       if (!gs_utils_is_current_desktop ("Unity")) {
+               g_notification_add_button_with_target (notification, C_("updates", "Review"), "app.set-mode", 
"s", "updated");
+               g_notification_set_default_action_and_target (notification, "app.set-mode", "s", "updated");
+       }
        g_application_send_notification (monitor->application, "offline-updates", notification);
 
        /* update the timestamp so we don't show again */
diff --git a/src/gtk-style-hc.css b/src/gtk-style-hc.css
index dfc0b87..5869d5b 100644
--- a/src/gtk-style-hc.css
+++ b/src/gtk-style-hc.css
@@ -113,9 +113,15 @@
 }
 
 .install-progress {
-       background-image: linear-gradient(to top, @theme_selected_bg_color 2px, 
alpha(@theme_selected_bg_color, 0) 2px);
+       background-image: linear-gradient(to top, @theme_selected_bg_color 4px, 
alpha(@theme_selected_bg_color, 0) 4px);
        background-repeat: no-repeat;
        background-position: 0 bottom;
+       border: 1px solid;
+       border-radius: 3px;
+       border-image-source: none;
+       border-image-width: 1;
+       border-image-slice: 100%;
+       border-image-repeat: stretch;
        transition: none;
 }
 
diff --git a/src/gtk-style.css b/src/gtk-style.css
index 42c3d1b..425c51c 100644
--- a/src/gtk-style.css
+++ b/src/gtk-style.css
@@ -144,9 +144,15 @@
 }
 
 .install-progress {
-       background-image: linear-gradient(to top, @theme_selected_bg_color 2px, 
alpha(@theme_selected_bg_color, 0) 2px);
+       background-image: linear-gradient(to top, @theme_selected_bg_color 4px, 
alpha(@theme_selected_bg_color, 0) 4px);
        background-repeat: no-repeat;
        background-position: 0 bottom;
+       border: 1px solid;
+       border-radius: 3px;
+       border-image-source: none;
+       border-image-width: 1;
+       border-image-slice: 100%;
+       border-image-repeat: stretch;
        transition: none;
 }
 
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
index bab4501..00dc96c 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -15,6 +15,8 @@ AM_CPPFLAGS =                                         \
        $(OSTREE_CFLAGS)                                \
        $(FLATPAK_CFLAGS)                               \
        $(RPM_CFLAGS)                                   \
+       $(OAUTH_CFLAGS)                                 \
+       $(LIBSECRET_CFLAGS)                             \
        -DI_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE    \
        -DBINDIR=\"$(bindir)\"                          \
        -DDATADIR=\"$(datadir)\"                        \
@@ -42,7 +44,13 @@ plugin_LTLIBRARIES =                                 \
        libgs_plugin_provenance-license.la              \
        libgs_plugin_fedora-tagger-usage.la             \
        libgs_plugin_epiphany.la                        \
-       libgs_plugin_icons.la
+       libgs_plugin_icons.la                           \
+       libgs_plugin_snappy.la
+
+if HAVE_APT
+plugin_LTLIBRARIES +=                                  \
+       libgs_plugin_apt.la
+endif
 
 if HAVE_PACKAGEKIT
 plugin_LTLIBRARIES +=                                  \
@@ -199,6 +207,27 @@ libgs_plugin_steam_la_LDFLAGS = -module -avoid-version
 libgs_plugin_steam_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
 endif
 
+if HAVE_APT
+ubuntu-unity-launcher-proxy.c ubuntu-unity-launcher-proxy.h: com.canonical.Unity.Launcher.xml Makefile
+       $(AM_V_GEN) gdbus-codegen --interface-prefix com.canonical.Unity.Launcher \
+                        --generate-c-code ubuntu-unity-launcher-proxy \
+                        --c-namespace UbuntuUnity \
+                        --annotate 'com.canonical.Unity.Launcher' \
+                                   org.gtk.GDBus.C.Name \
+                                   Launcher \
+                        $<
+
+CLEANFILES = ubuntu-unity-launcher-proxy.h ubuntu-unity-launcher-proxy.c
+
+libgs_plugin_apt_la_SOURCES = \
+       ubuntu-unity-launcher-proxy.c \
+       ubuntu-unity-launcher-proxy.h \
+       gs-plugin-apt.cc
+libgs_plugin_apt_la_LIBADD = $(GS_PLUGIN_LIBS) -lapt-pkg
+libgs_plugin_apt_la_LDFLAGS = -module -avoid-version
+libgs_plugin_apt_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+endif
+
 libgs_plugin_menu_spec_categories_la_SOURCES =         \
        gs-plugin-menu-spec-categories.c                \
        menu-spec-common.c                              \
@@ -232,8 +261,20 @@ libgs_plugin_hardcoded_featured_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
 
 if HAVE_UBUNTU_REVIEWS
 libgs_plugin_ubuntu_reviews_la_SOURCES =               \
-       gs-plugin-ubuntu-reviews.c
-libgs_plugin_ubuntu_reviews_la_LIBADD = $(GS_PLUGIN_LIBS) $(SOUP_LIBS) $(JSON_GLIB_LIBS) $(SQLITE_LIBS)
+       gs-plugin-ubuntu-reviews.c                      \
+       gs-ubuntuone.h                                  \
+       gs-ubuntuone.c                                  \
+       gs-ubuntuone-dialog.h                           \
+       gs-ubuntuone-dialog.c                           \
+       gs-ubuntu-snapd.h                               \
+       gs-ubuntu-snapd.c
+libgs_plugin_ubuntu_reviews_la_LIBADD =                        \
+       $(GS_PLUGIN_LIBS)                               \
+       $(SOUP_LIBS)                                    \
+       $(JSON_GLIB_LIBS)                               \
+       $(OAUTH_LIBS)                                   \
+       $(SQLITE_LIBS)                                  \
+       $(LIBSECRET_LIBS)
 libgs_plugin_ubuntu_reviews_la_LDFLAGS = -module -avoid-version
 libgs_plugin_ubuntu_reviews_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
 endif
@@ -315,6 +356,22 @@ libgs_plugin_packagekit_proxy_la_LIBADD = $(GS_PLUGIN_LIBS)
 libgs_plugin_packagekit_proxy_la_LDFLAGS = -module -avoid-version
 libgs_plugin_packagekit_proxy_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
 
+libgs_plugin_snappy_la_SOURCES =                       \
+       gs-plugin-snappy.c                              \
+       gs-ubuntuone.h                                  \
+       gs-ubuntuone.c                                  \
+       gs-ubuntuone-dialog.h                           \
+       gs-ubuntuone-dialog.c                           \
+       gs-ubuntu-snapd.h                               \
+       gs-ubuntu-snapd.c
+libgs_plugin_snappy_la_LIBADD =                                \
+       $(GS_PLUGIN_LIBS)                               \
+       $(SOUP_LIBS)                                    \
+       $(JSON_GLIB_LIBS)                               \
+       $(LIBSECRET_LIBS)
+libgs_plugin_snappy_la_LDFLAGS = -module -avoid-version
+libgs_plugin_snappy_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+
 if ENABLE_TESTS
 check_PROGRAMS =                                               \
        gs-self-test
@@ -332,4 +389,6 @@ gs_self_test_CFLAGS = $(WARN_CFLAGS)
 TESTS = gs-self-test
 endif
 
+EXTRA_DIST = gs-ubuntuone-dialog.h gs-ubuntuone-dialog.ui ubuntu-one.png com.canonical.Unity.Launcher.xml
+
 -include $(top_srcdir)/git.mk
diff --git a/src/plugins/com.canonical.Unity.Launcher.xml b/src/plugins/com.canonical.Unity.Launcher.xml
new file mode 100644
index 0000000..b631a2d
--- /dev/null
+++ b/src/plugins/com.canonical.Unity.Launcher.xml
@@ -0,0 +1,15 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd";>
+<node>
+  <interface name='com.canonical.Unity.Launcher'>
+    <method name='AddLauncherItem'>
+      <arg type='s' name='appstream_app_id' direction='in'/>
+      <arg type='s' name='aptdaemon_task' direction='in'/>
+    </method>
+    
+    <method name='UpdateLauncherIconFavoriteState'>
+      <arg type='s' name='icon_uri' direction='in'/>
+      <arg type='b' name='is_sticky' direction='in'/>
+    </method>
+  </interface>
+</node>
diff --git a/src/plugins/gs-plugin-appstream.c b/src/plugins/gs-plugin-appstream.c
index 244f223..edb4a45 100644
--- a/src/plugins/gs-plugin-appstream.c
+++ b/src/plugins/gs-plugin-appstream.c
@@ -216,6 +216,21 @@ gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error)
        return TRUE;
 }
 
+gboolean
+gs_plugin_refresh (GsPlugin              *plugin,
+                  guint                  cache_age,
+                  GsPluginRefreshFlags   flags,
+                  GCancellable          *cancellable,
+                  GError               **error)
+{
+       if (flags & GS_PLUGIN_REFRESH_FLAGS_UI) {
+               gs_plugin_setup (plugin, cancellable, error);
+               gs_plugin_updates_changed (plugin);
+       }
+
+       return TRUE;
+}
+
 static gboolean
 gs_plugin_refine_from_id (GsPlugin *plugin,
                          GsApp *app,
diff --git a/src/plugins/gs-plugin-apt.cc b/src/plugins/gs-plugin-apt.cc
new file mode 100644
index 0000000..a5eeace
--- /dev/null
+++ b/src/plugins/gs-plugin-apt.cc
@@ -0,0 +1,1154 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <config.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+#include <apt-pkg/init.h>
+#include <apt-pkg/cachefile.h>
+#include <apt-pkg/cmndline.h>
+#include <apt-pkg/version.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string>
+#include <set>
+#include <vector>
+#include <fstream>
+#include <iomanip>
+#include <algorithm>
+#include <stdexcept>
+
+#include <gs-plugin.h>
+#include <gs-utils.h>
+
+#define LICENSE_URL "http://www.ubuntu.com/about/about-ubuntu/licensing";
+
+#define INFO_DIR "/var/lib/dpkg/info"
+
+typedef struct
+{
+       GMutex pending_mutex;
+       GCond pending_cond;
+
+       GMutex dispatched_mutex;
+       GCond dispatched_cond;
+
+       GMutex hashtable_mutex;
+
+       guint still_to_read;
+       guint dispatched_reads;
+
+       GsPlugin *plugin;
+} ReadListData;
+
+typedef struct {
+       gchar           *name;
+       gchar           *section;
+       gchar           *installed_version;
+       gchar           *update_version;
+       gchar           *origin;
+       gchar           *release;
+       gchar           *component;
+       gint             installed_size;
+} PackageInfo;
+
+#include "ubuntu-unity-launcher-proxy.h"
+
+struct GsPluginData {
+       GMutex           mutex;
+       gboolean         loaded;
+       GHashTable      *package_info;
+       GHashTable      *installed_files;
+       GList           *installed_packages;
+       GList           *updatable_packages;
+};
+
+const gchar *
+gs_plugin_get_name (void)
+{
+       return "apt";
+}
+
+const gchar **
+gs_plugin_order_after (GsPlugin *plugin)
+{
+       static const gchar *deps[] = {
+               "appstream",            /* need pkgname */
+               NULL };
+       return deps;
+}
+
+/**
+ * gs_plugin_get_conflicts:
+ */
+const gchar **
+gs_plugin_get_conflicts (GsPlugin *plugin)
+{
+
+       static const gchar *deps[] = {
+               "packagekit",
+               "packagekit-history",
+               "packagekit-offline",
+               "packagekit-origin",
+               "packagekit-proxy",
+               "packagekit-refine",
+               "packagekit-refresh",
+               "systemd-updates",
+               NULL };
+       return deps;
+}
+
+static void
+free_package_info (gpointer data)
+{
+       PackageInfo *info = (PackageInfo *) data;
+       g_free (info->section);
+       g_free (info->installed_version);
+       g_free (info->update_version);
+       g_free (info->origin);
+       g_free (info->release);
+       g_free (info->component);
+       g_free (info->name);
+       g_free (info);
+}
+
+void
+gs_plugin_initialize (GsPlugin *plugin)
+{
+       GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData));
+
+       priv->package_info = g_hash_table_new_full (g_str_hash,
+                                                   g_str_equal,
+                                                   NULL,
+                                                   free_package_info);
+
+       priv->installed_files = g_hash_table_new_full (g_str_hash,
+                                                      g_str_equal,
+                                                      g_free,
+                                                      g_free);
+
+       g_mutex_init (&priv->mutex);
+
+       pkgInitConfig (*_config);
+       pkgInitSystem (*_config, _system);
+}
+
+void
+gs_plugin_destroy (GsPlugin *plugin)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+
+       g_mutex_lock (&priv->mutex);
+       priv->loaded = FALSE;
+       g_clear_pointer (&priv->package_info, g_hash_table_unref);
+       g_clear_pointer (&priv->installed_files, g_hash_table_unref);
+       g_clear_pointer (&priv->installed_packages, g_list_free);
+       g_clear_pointer (&priv->updatable_packages, g_list_free);
+       g_mutex_unlock (&priv->mutex);
+       g_mutex_clear (&priv->mutex);
+}
+
+
+static void
+read_list_file_cb (GObject *object,
+                  GAsyncResult *res,
+                  gpointer user_data)
+{
+       g_autoptr(GFileInputStream) stream = NULL;
+       g_autoptr(GFile) file = NULL;
+       ReadListData *data;
+       g_autofree gchar *buffer = NULL;
+       g_autofree gchar *filename = NULL;
+       g_autoptr(GFileInfo) info = NULL;
+       g_auto(GStrv) file_lines = NULL;
+       g_auto(GStrv) file_components = NULL;
+       gchar *line;
+
+       file = G_FILE (object);
+       data = (ReadListData *) user_data;
+       stream = g_file_read_finish (file, res, NULL);
+
+       info = g_file_input_stream_query_info (stream,
+                                              G_FILE_ATTRIBUTE_STANDARD_SIZE,
+                                              NULL,
+                                              NULL);
+
+       if (!info)
+               return;
+
+       if (!g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_STANDARD_SIZE))
+               return;
+
+       buffer = (gchar *) g_malloc0 (g_file_info_get_size (info) + 1);
+
+       if (!g_input_stream_read_all (G_INPUT_STREAM (stream),
+                                     buffer,
+                                     g_file_info_get_size (info),
+                                     NULL,
+                                     NULL,
+                                     NULL))
+               return;
+
+       g_input_stream_close (G_INPUT_STREAM (stream), NULL, NULL);
+
+       file_lines = g_strsplit (buffer, "\n", -1);
+
+       filename = g_file_get_basename (file);
+       file_components = g_strsplit (filename, ".", 2);
+
+       for (int i = 0; file_lines[i]; ++i)
+               if (g_str_has_suffix (file_lines[i], ".desktop") ||
+                   g_str_has_suffix (file_lines[i], ".metainfo.xml") ||
+                   g_str_has_suffix (file_lines[i], ".appdata.xml"))
+               {
+                       g_mutex_lock (&data->hashtable_mutex);
+                       /* filename -> package */
+                       g_hash_table_insert (gs_plugin_get_data (data->plugin)->installed_files,
+                                            g_strdup (file_lines[i]),
+                                            g_strdup (file_components[0]));
+                       g_mutex_unlock (&data->hashtable_mutex);
+               }
+
+       g_mutex_lock (&data->dispatched_mutex);
+       (data->dispatched_reads)--;
+       g_cond_signal(&data->dispatched_cond);
+       g_mutex_unlock(&data->dispatched_mutex);
+
+       g_mutex_lock (&data->pending_mutex);
+       (data->still_to_read)--;
+       g_cond_signal (&data->pending_cond);
+       g_mutex_unlock (&data->pending_mutex);
+}
+
+static void
+read_list_file (GList *files,
+               ReadListData *data)
+{
+       GFile *gfile;
+       GList *files_iter;
+
+       for (files_iter = files; files_iter; files_iter = files_iter->next)
+       {
+               /* freed in read_list_file_cb */
+               gfile = g_file_new_for_path ((gchar *) files_iter->data);
+
+               g_mutex_lock (&data->dispatched_mutex);
+               g_file_read_async (gfile,
+                               G_PRIORITY_DEFAULT,
+                               NULL,
+                               read_list_file_cb,
+                               data);
+
+               (data->dispatched_reads)++;
+
+               while (data->dispatched_reads >= 500)
+                       g_cond_wait (&data->dispatched_cond, &data->dispatched_mutex);
+
+               g_mutex_unlock (&data->dispatched_mutex);
+       }
+}
+
+static void
+look_for_files (GsPlugin *plugin)
+{
+
+       ReadListData data;
+       GList *files = NULL;
+
+       data.still_to_read = 0;
+       data.dispatched_reads = 0;
+       g_cond_init (&data.pending_cond);
+       g_mutex_init (&data.pending_mutex);
+       g_cond_init (&data.dispatched_cond);
+       g_mutex_init (&data.dispatched_mutex);
+       g_mutex_init (&data.hashtable_mutex);
+       data.plugin = plugin;
+
+       g_autoptr (GDir) dir = NULL;
+       const gchar *file;
+
+       dir = g_dir_open (INFO_DIR, 0, NULL);
+
+       while (file = g_dir_read_name (dir))
+               if (g_str_has_suffix (file, ".list") &&
+                  /* app-install-data contains loads of .desktop files, but they aren't installed by it */
+                  (!g_strcmp0 (file, "app-install-data.list") == 0))
+               {
+                       files = g_list_append (files, g_build_filename (INFO_DIR, file, NULL));
+                       data.still_to_read++;
+               }
+
+       read_list_file (files, &data);
+
+       /* Wait until all the reads are done */
+       g_mutex_lock (&data.pending_mutex);
+       while (data.still_to_read > 0)
+               g_cond_wait (&data.pending_cond, &data.pending_mutex);
+       g_mutex_unlock (&data.pending_mutex);
+
+       g_mutex_clear (&data.pending_mutex);
+       g_cond_clear (&data.pending_cond);
+       g_mutex_clear (&data.dispatched_mutex);
+       g_cond_clear (&data.dispatched_cond);
+       g_mutex_clear (&data.hashtable_mutex);
+
+       g_list_free_full (files, g_free);
+}
+
+static gboolean
+version_newer (const gchar *v0, const gchar *v1)
+{
+       return v0 ? _system->VS->CmpVersion(v0, v1) < 0 : TRUE;
+}
+
+/* return FALSE for a fatal error */
+static gboolean
+look_at_pkg (const pkgCache::PkgIterator &P,
+            pkgSourceList *list,
+            pkgPolicy *policy,
+            GsPlugin *plugin,
+            GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+       pkgCache::VerIterator current = P.CurrentVer();
+       pkgCache::VerIterator candidate = policy->GetCandidateVer(P);
+       pkgCache::VerFileIterator VF;
+       FileFd PkgF;
+       pkgTagSection Tags;
+       gchar *name;
+
+       PackageInfo *info;
+
+       if (!candidate || !candidate.FileList ())
+               return TRUE;
+
+       name = g_strdup (P.Name ());
+       info = (PackageInfo *) g_hash_table_lookup (priv->package_info, name);
+       if (info == NULL) {
+               info = g_new0 (PackageInfo, 1);
+               info->name = name;
+               g_hash_table_insert (priv->package_info, name, info);
+       } else
+               g_free (name);
+
+       for (VF = candidate.FileList (); VF.IsGood (); VF++) {
+               // see InRelease for the fields
+               if (VF.File ().Archive ())
+                       info->release = g_strdup (VF.File ().Archive ());
+               if (VF.File ().Origin ())
+                       info->origin = g_strdup (VF.File ().Origin ());
+               if (VF.File ().Component ())
+                       info->component = g_strdup (VF.File ().Component ());
+               // also available: Codename, Label
+               break;
+       }
+
+
+       pkgCache::PkgFileIterator I = VF.File ();
+
+       if (I.IsOk () == false) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            ("apt DB load failed: package file %s is out of sync."), I.FileName ());
+               return FALSE;
+       }
+
+       PkgF.Open (I.FileName (), FileFd::ReadOnly, FileFd::Extension);
+
+       pkgTagFile TagF (&PkgF);
+
+       if (TagF.Jump (Tags, current.FileList ()->Offset) == false) {
+               if (TagF.Jump (Tags, candidate.FileList ()->Offset) == false)
+                       return TRUE;
+       }
+
+       if (Tags.FindI ("Installed-Size") > 0)
+               info->installed_size = Tags.FindI ("Installed-Size")*1024;
+       else
+               info->installed_size = 0;
+
+       if (current)
+               info->installed_version = g_strdup (current.VerStr ());
+       if (candidate)
+               info->update_version = g_strdup (candidate.VerStr ());
+
+       info->section = g_strdup (candidate.Section ());
+       if (info->installed_version) {
+               priv->installed_packages = g_list_append (priv->installed_packages, info);
+       }
+
+       /* no upgrade */
+       if (g_strcmp0 (info->installed_version, info->update_version) == 0)
+               g_clear_pointer (&info->update_version, g_free);
+
+       if (info->installed_version && info->update_version)
+               priv->updatable_packages = g_list_append (priv->updatable_packages, info);
+
+       return TRUE;
+}
+
+static gboolean
+load_apt_db (GsPlugin *plugin, GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+       pkgSourceList *list;
+       pkgPolicy *policy;
+       pkgCacheFile cachefile;
+       pkgCache *cache;
+       pkgCache::PkgIterator P;
+       g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+       if (priv->loaded)
+               return TRUE;
+
+       _error->Discard();
+       cache = cachefile.GetPkgCache();
+       list = cachefile.GetSourceList();
+       policy = cachefile.GetPolicy();
+       if (cache == NULL || _error->PendingError()) {
+               _error->DumpErrors();
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "apt DB load failed: error while initialising");
+               return FALSE;
+       }
+
+       for (pkgCache::GrpIterator grp = cache->GrpBegin(); grp != cache->GrpEnd(); grp++) {
+               P = grp.FindPreferredPkg();
+               if (P.end())
+                       continue;
+               if (!look_at_pkg (P, list, policy, plugin, error))
+                       return FALSE;
+       }
+
+       /* load filename -> package map into priv->installed_files */
+       look_for_files (plugin);
+
+       priv->loaded = TRUE;
+       return TRUE;
+}
+
+static void
+unload_apt_db (GsPlugin *plugin)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+       g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+       priv->loaded = FALSE;
+       g_hash_table_remove_all (priv->package_info);
+       g_hash_table_remove_all (priv->installed_files);
+       g_clear_pointer (&priv->installed_packages, g_list_free);
+       g_clear_pointer (&priv->updatable_packages, g_list_free);
+}
+
+static void
+get_changelog (GsPlugin *plugin, GsApp *app)
+{
+       guint i;
+       guint status_code;
+       g_autofree gchar *binary_source = NULL;
+       g_autofree gchar *changelog_prefix = NULL;
+       g_autofree gchar *current_version = NULL;
+       g_autofree gchar *source_prefix = NULL;
+       g_autofree gchar *update_version = NULL;
+       g_autofree gchar *uri = NULL;
+       g_auto(GStrv) lines = NULL;
+       g_autoptr(GString) details = NULL;
+       g_autoptr(SoupMessage) msg = NULL;
+
+       // Need to know the source and version to download changelog
+       binary_source = g_strdup (gs_app_get_source_default (app));
+       current_version = g_strdup (gs_app_get_version (app));
+       update_version = g_strdup (gs_app_get_update_version (app));
+       if (binary_source == NULL || update_version == NULL)
+               return;
+
+       if (g_str_has_prefix (binary_source, "lib"))
+               source_prefix = g_strdup_printf ("lib%c", binary_source[3]);
+       else
+               source_prefix = g_strdup_printf ("%c", binary_source[0]);
+       uri = g_strdup_printf ("http://changelogs.ubuntu.com/changelogs/binary/%s/%s/%s/changelog";, 
source_prefix, binary_source, update_version);
+
+       /* download file */
+       msg = soup_message_new (SOUP_METHOD_GET, uri);
+       status_code = soup_session_send_message (gs_plugin_get_soup_session (plugin), msg);
+       if (status_code != SOUP_STATUS_OK) {
+               g_warning ("Failed to get changelog for %s version %s from changelogs.ubuntu.com: %s", 
binary_source, update_version, soup_status_get_phrase (status_code));
+               return;
+       }
+
+       // Extract changelog entries newer than our current version
+       lines = g_strsplit (msg->response_body->data, "\n", -1);
+       details = g_string_new ("");
+       for (i = 0; lines[i] != NULL; i++) {
+               gchar *line = lines[i];
+               const gchar *version_start, *version_end;
+               g_autofree gchar *v = NULL;
+
+               // First line is in the form "package (version) distribution(s); urgency=urgency"
+                version_start = strchr (line, '(');
+                version_end = strchr (line, ')');
+               if (line[0] == ' ' || version_start == NULL || version_end == NULL || version_end < 
version_start)
+                       continue;
+               v = g_strdup_printf ("%.*s", (int) (version_end - version_start - 1), version_start + 1);
+
+               // We're only interested in new versions
+               if (!version_newer (current_version, v))
+                       break;
+
+               g_string_append_printf (details, "%s\n", v);
+               for (i++; lines[i] != NULL; i++) {
+                       // Last line is in the form " -- maintainer name <email address>  date"
+                       if (g_str_has_prefix (lines[i], " -- "))
+                               break;
+                       g_string_append_printf (details, "%s\n", lines[i]);
+               }
+       }
+
+       gs_app_set_update_details (app, details->str);
+}
+
+static gboolean
+is_official (PackageInfo *info)
+{
+       return g_strcmp0 (info->origin, "Ubuntu") == 0;
+}
+
+static gboolean
+is_open_source (PackageInfo *info)
+{
+       const gchar *open_source_components[] = { "main", "universe", NULL };
+
+       /* There's no valid apps in the libs section */
+       return info->component != NULL && g_strv_contains (open_source_components, info->component);
+}
+
+static gchar *
+get_origin (PackageInfo *info)
+{
+       if (!info->origin)
+               return NULL;
+
+       g_autofree gchar *origin_lower = g_strdup (info->origin);
+       for (int i = 0; origin_lower[i]; ++i)
+               origin_lower[i] = g_ascii_tolower (origin_lower[i]);
+
+       return g_strdup_printf ("%s-%s-%s", origin_lower, info->release, info->component);
+}
+
+gboolean
+gs_plugin_refine (GsPlugin *plugin,
+                 GList **list,
+                 GsPluginRefineFlags flags,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+       GList *link;
+       GsApp *app;
+       PackageInfo *info;
+       const gchar *tmp;
+       g_autoptr(GMutexLocker) locker = NULL;
+
+       if (!load_apt_db (plugin, error))
+               return FALSE;
+
+       locker = g_mutex_locker_new (&priv->mutex);
+
+       for (link = *list; link; link = link->next) {
+               app = (GsApp *) link->data;
+               g_autofree gchar *fn = NULL;
+               g_autofree gchar *origin = NULL;
+               gchar *package = NULL;
+
+               tmp = gs_app_get_id (app);
+               if (gs_app_get_source_id_default (app) == NULL && tmp) {
+                       switch (gs_app_get_kind (app)) {
+                       case AS_APP_KIND_DESKTOP:
+                               fn = g_strdup_printf ("/usr/share/applications/%s", tmp);
+                               break;
+                       case AS_APP_KIND_ADDON:
+                               fn = g_strdup_printf ("/usr/share/appdata/%s.metainfo.xml", tmp);
+                               break;
+                       default:
+                               break;
+                       }
+
+                       if (!g_file_test (fn, G_FILE_TEST_EXISTS)) {
+                               g_debug ("ignoring %s as does not exist", fn);
+                       } else {
+                               package = (gchar *) g_hash_table_lookup (priv->installed_files,
+                                                                        fn);
+                               if (package != NULL) {
+                                       gs_app_add_source (app, package);
+                                       gs_app_set_management_plugin (app, "apt");
+                               }
+                       }
+               }
+
+               if (gs_app_get_source_default (app) == NULL)
+                       continue;
+
+               info = (PackageInfo *) g_hash_table_lookup (priv->package_info, gs_app_get_source_default 
(app));
+               if (info == NULL)
+                       continue;
+
+               origin = get_origin (info);
+               gs_app_set_origin (app, origin);
+               gs_app_set_origin_ui (app, info->origin);
+
+               if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) {
+                       if (info->installed_version != NULL) {
+                               if (info->update_version != NULL) {
+                                       gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE);
+                               } else {
+                                       gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+                               }
+                       } else {
+                               gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+                       }
+               }
+               if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) != 0) {
+                       g_autofree gchar *origin = get_origin (info);
+                       gs_app_set_origin (app, origin);
+                       gs_app_set_origin_ui (app, info->origin);
+               }
+               if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) != 0 && gs_app_get_size_installed (app) == 
0) {
+                       gs_app_set_size_installed (app, info->installed_size);
+               }
+               if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) != 0) {
+                       if (info->installed_version != NULL) {
+                               gs_app_set_version (app, info->installed_version);
+                       } else {
+                               gs_app_set_version (app, info->update_version);
+                       }
+                       if (info->update_version != NULL) {
+                               gs_app_set_update_version (app, info->update_version);
+                       }
+               }
+               if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) != 0 && is_open_source(info)) {
+                       gs_app_set_license (app, GS_APP_QUALITY_LOWEST, "@LicenseRef-free=" LICENSE_URL);
+               }
+
+               if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) != 0) {
+                       get_changelog (plugin, app);
+               }
+       }
+
+       return TRUE;
+}
+
+static gboolean
+is_allowed_section (PackageInfo *info)
+{
+       const gchar *section_blacklist[] = { "libs", NULL };
+
+       /* There's no valid apps in the libs section */
+       return info->section == NULL || !g_strv_contains (section_blacklist, info->section);
+}
+
+gboolean
+gs_plugin_add_installed (GsPlugin *plugin,
+                        GsAppList *list,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+       GList *link;
+       g_autoptr(GMutexLocker) locker = NULL;
+
+       if (!load_apt_db (plugin, error))
+               return FALSE;
+
+       locker = g_mutex_locker_new (&priv->mutex);
+
+       for (link = priv->installed_packages; link; link = link->next) {
+               PackageInfo *info = (PackageInfo *) link->data;
+               g_autofree gchar *origin = get_origin (info);
+               g_autoptr(GsApp) app = NULL;
+
+               if (!is_allowed_section (info))
+                       continue;
+
+               app = gs_app_new (NULL);
+               gs_app_set_management_plugin (app, "apt");
+               gs_app_set_name (app, GS_APP_QUALITY_LOWEST, info->name);
+               gs_app_add_source (app, info->name);
+               gs_app_set_origin (app, origin);
+               gs_app_set_origin_ui (app, info->origin);
+               gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+               gs_app_list_add (list, app);
+       }
+
+       return TRUE;
+}
+
+typedef struct {
+       GsPlugin *plugin;
+       GMainLoop *loop;
+       GsApp *app;
+       GList *apps;
+       gchar **result;
+} TransactionData;
+
+static void
+transaction_property_changed_cb (GDBusConnection *connection,
+                                const gchar *sender_name,
+                                const gchar *object_path,
+                                const gchar *interface_name,
+                                const gchar *signal_name,
+                                GVariant *parameters,
+                                gpointer user_data)
+{
+       TransactionData *data = (TransactionData *) user_data;
+       const gchar *name;
+       GList *i;
+       g_autoptr(GVariant) value = NULL;
+
+       if (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(sv)"))) {
+               g_variant_get (parameters, "(&sv)", &name, &value);
+               if (g_strcmp0 (name, "Progress") == 0) {
+                       if (data->app)
+                               gs_app_set_progress (data->app, g_variant_get_int32 (value));
+                       for (i = data->apps; i != NULL; i = i->next)
+                               gs_app_set_progress (GS_APP (i->data), g_variant_get_int32 (value));
+               }
+       } else {
+               g_warning ("Unknown parameters in %s.%s: %s", interface_name, signal_name, 
g_variant_get_type_string (parameters));
+       }
+}
+
+static void
+transaction_finished_cb (GDBusConnection *connection,
+                        const gchar *sender_name,
+                        const gchar *object_path,
+                        const gchar *interface_name,
+                        const gchar *signal_name,
+                        GVariant *parameters,
+                        gpointer user_data)
+{
+       TransactionData *data = (TransactionData *) user_data;
+
+       if (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(s)")))
+               g_variant_get (parameters, "(s)", data->result);
+       else
+               g_warning ("Unknown parameters in %s.%s: %s", interface_name, signal_name, 
g_variant_get_type_string (parameters));
+
+       g_main_loop_quit (data->loop);
+}
+
+static void
+notify_unity_launcher (GsApp *app, const gchar *transaction_path)
+{
+       UbuntuUnityLauncher *launcher = NULL;
+
+       g_return_if_fail (GS_IS_APP (app));
+       g_return_if_fail (transaction_path);
+
+       launcher = ubuntu_unity_launcher_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
+               G_DBUS_PROXY_FLAGS_NONE,
+               "com.canonical.Unity.Launcher",
+               "/com/canonical/Unity/Launcher",
+               NULL, NULL);
+
+       g_return_if_fail (launcher);
+
+       ubuntu_unity_launcher_call_add_launcher_item (launcher,
+               gs_app_get_id (app),
+               transaction_path,
+               NULL, NULL, NULL);
+
+       g_object_unref (launcher);
+}
+
+static gboolean
+aptd_transaction (GsPlugin     *plugin,
+                 const gchar  *method,
+                 GsApp        *app,
+                 GList        *apps,
+                 GVariant     *parameters,
+                 GError      **error)
+{
+       g_autoptr(GDBusConnection) conn = NULL;
+       g_autoptr(GVariant) result = NULL;
+       g_autofree gchar *transaction_path = NULL, *transaction_result = NULL;
+       g_autoptr(GMainLoop) loop = NULL;
+       guint property_signal, finished_signal;
+       TransactionData data;
+
+       conn = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, error);
+       if (conn == NULL)
+               return FALSE;
+
+       if (parameters == NULL && app != NULL)
+               parameters = g_variant_new_parsed ("([%s],)", gs_app_get_source_default (app));
+
+       result = g_dbus_connection_call_sync (conn,
+                                             "org.debian.apt",
+                                             "/org/debian/apt",
+                                             "org.debian.apt",
+                                             method,
+                                             parameters,
+                                             G_VARIANT_TYPE ("(s)"),
+                                             G_DBUS_CALL_FLAGS_NONE,
+                                             -1,
+                                             NULL,
+                                             error);
+       if (result == NULL)
+               return FALSE;
+       g_variant_get (result, "(s)", &transaction_path);
+       g_variant_unref (result);
+
+       if (!g_strcmp0(method, "InstallPackages"))
+               notify_unity_launcher (app, transaction_path);
+
+       loop = g_main_loop_new (NULL, FALSE);
+
+       data.plugin = plugin;
+       data.app = app;
+       data.apps = apps;
+       data.loop = loop;
+       data.result = &transaction_result;
+       property_signal = g_dbus_connection_signal_subscribe (conn,
+                                                             "org.debian.apt",
+                                                             "org.debian.apt.transaction",
+                                                             "PropertyChanged",
+                                                             transaction_path,
+                                                             NULL,
+                                                             G_DBUS_SIGNAL_FLAGS_NONE,
+                                                             transaction_property_changed_cb,
+                                                             &data,
+                                                             NULL);
+       finished_signal = g_dbus_connection_signal_subscribe (conn,
+                                                             "org.debian.apt",
+                                                             "org.debian.apt.transaction",
+                                                             "Finished",
+                                                             transaction_path,
+                                                             NULL,
+                                                             G_DBUS_SIGNAL_FLAGS_NONE,
+                                                             transaction_finished_cb,
+                                                             &data,
+                                                             NULL);
+       result = g_dbus_connection_call_sync (conn,
+                                             "org.debian.apt",
+                                             transaction_path,
+                                             "org.debian.apt.transaction",
+                                             "Run",
+                                             g_variant_new ("()"),
+                                             G_VARIANT_TYPE ("()"),
+                                             G_DBUS_CALL_FLAGS_NONE,
+                                             -1,
+                                             NULL,
+                                             error);
+       if (result != NULL)
+               g_main_loop_run (loop);
+       g_dbus_connection_signal_unsubscribe (conn, property_signal);
+       g_dbus_connection_signal_unsubscribe (conn, finished_signal);
+       if (result == NULL)
+               return FALSE;
+
+       if (g_strcmp0 (transaction_result, "exit-success") != 0) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "apt transaction returned result %s", transaction_result);
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+app_is_ours (GsApp *app)
+{
+       const gchar *management_plugin = gs_app_get_management_plugin (app);
+
+       // FIXME: Since appstream marks all packages as owned by PackageKit and
+       // we are replacing PackageKit we need to accept those packages
+       const gchar *our_management_plugins[] = { "PackageKit", "apt", NULL };
+
+       return g_strv_contains (our_management_plugins, management_plugin);
+}
+
+gboolean
+gs_plugin_app_install (GsPlugin *plugin,
+                      GsApp *app,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       g_autofree gchar *filename = NULL;
+       gboolean success = FALSE;
+
+       if (!app_is_ours (app))
+               return TRUE;
+
+       if (gs_app_get_source_default (app) == NULL)
+               return TRUE;
+
+       switch (gs_app_get_state (app)) {
+       case AS_APP_STATE_AVAILABLE:
+       case AS_APP_STATE_UPDATABLE:
+               gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+               success = aptd_transaction (plugin, "InstallPackages", app, NULL, NULL, error);
+               break;
+       case AS_APP_STATE_AVAILABLE_LOCAL:
+               filename = g_file_get_path (gs_app_get_local_file (app));
+               gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+               success = aptd_transaction (plugin, "InstallFile", app, NULL,
+                                           g_variant_new_parsed ("(%s, true)", filename),
+                                           error);
+               break;
+       default:
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "do not know how to install app in state %s",
+                            as_app_state_to_string (gs_app_get_state (app)));
+               return FALSE;
+       }
+
+
+       if (success)
+               gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+       else
+               gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+
+       return success;
+}
+
+gboolean
+gs_plugin_app_remove (GsPlugin *plugin,
+                     GsApp *app,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       if (!app_is_ours (app))
+               return TRUE;
+
+       if (gs_app_get_source_default (app) == NULL)
+               return TRUE;
+
+       gs_app_set_state (app, AS_APP_STATE_REMOVING);
+       if (aptd_transaction (plugin, "RemovePackages", app, NULL, NULL, error))
+               gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+       else {
+               gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_refresh (GsPlugin *plugin,
+                  guint cache_age,
+                  GsPluginRefreshFlags flags,
+                  GCancellable *cancellable,
+                  GError **error)
+{
+       if ((flags & (GS_PLUGIN_REFRESH_FLAGS_METADATA | GS_PLUGIN_REFRESH_FLAGS_PAYLOAD)) == 0)
+               return TRUE;
+
+       if (!aptd_transaction (plugin, "UpdateCache", NULL, NULL, NULL, error))
+               return FALSE;
+
+       unload_apt_db (plugin);
+
+       gs_plugin_updates_changed (plugin);
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_add_updates (GsPlugin *plugin,
+                       GsAppList *list,
+                       GCancellable *cancellable,
+                       GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+       GList *link;
+       g_autoptr(GMutexLocker) locker = NULL;
+
+       if (!load_apt_db (plugin, error))
+               return FALSE;
+
+       locker = g_mutex_locker_new (&priv->mutex);
+
+       for (link = priv->updatable_packages; link; link = link->next) {
+               PackageInfo *info = (PackageInfo *) link->data;
+               g_autoptr(GsApp) app = NULL;
+
+               if (!is_allowed_section (info))
+                       continue;
+
+               app = gs_app_new (NULL);
+               gs_app_set_management_plugin (app, "apt");
+               gs_app_set_name (app, GS_APP_QUALITY_LOWEST, info->name);
+               gs_app_set_kind (app, AS_APP_KIND_GENERIC);
+               gs_app_add_source (app, info->name);
+               gs_app_list_add (list, app);
+       }
+
+       return TRUE;
+}
+
+static void
+set_list_state (GList      *apps,
+               AsAppState  state)
+{
+       GList *i;
+       guint j;
+       GsApp *app_i;
+       GsApp *app_j;
+       GPtrArray *related;
+
+       for (i = apps; i != NULL; i = i->next) {
+               app_i = GS_APP (i->data);
+               gs_app_set_state (app_i, state);
+
+               if (g_strcmp0 (gs_app_get_id (app_i), "os-update.virtual") == 0) {
+                       related = gs_app_get_related (app_i);
+
+                       for (j = 0; j < related->len; j++) {
+                               app_j = GS_APP (g_ptr_array_index (related, j));
+                               gs_app_set_state (app_j, state);
+                       }
+               }
+       }
+}
+
+gboolean
+gs_plugin_update (GsPlugin      *plugin,
+                 GList         *apps,
+                 GCancellable  *cancellable,
+                 GError       **error)
+{
+       GList *i;
+       GsApp *app_i;
+
+       for (i = apps; i != NULL; i = i->next) {
+               app_i = GS_APP (i->data);
+
+               if (g_strcmp0 (gs_app_get_id (app_i), "os-update.virtual") == 0) {
+                       set_list_state (apps, AS_APP_STATE_INSTALLING);
+
+                       if (aptd_transaction (plugin, "UpgradeSystem", NULL, apps, g_variant_new_parsed 
("(false,)"), error)) {
+                               set_list_state (apps, AS_APP_STATE_INSTALLED);
+
+                               unload_apt_db (plugin);
+
+                               gs_plugin_updates_changed (plugin);
+
+                               return TRUE;
+                       } else {
+                               set_list_state (apps, AS_APP_STATE_UPDATABLE_LIVE);
+
+                               return FALSE;
+                       }
+               }
+       }
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_update_app (GsPlugin *plugin,
+                     GsApp *app,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       GPtrArray *apps;
+       GsApp *app_i;
+       guint i;
+       GVariantBuilder builder;
+
+       if (g_strcmp0 (gs_app_get_id (app), "os-update.virtual") == 0) {
+               apps = gs_app_get_related (app);
+
+               g_variant_builder_init (&builder, G_VARIANT_TYPE ("(as)"));
+               g_variant_builder_open (&builder, G_VARIANT_TYPE ("as"));
+
+               gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+
+               for (i = 0; i < apps->len; i++) {
+                       app_i = GS_APP (g_ptr_array_index (apps, i));
+                       gs_app_set_state (app_i, AS_APP_STATE_INSTALLING);
+                       g_variant_builder_add (&builder, "s", gs_app_get_source_default (app_i));
+               }
+
+               g_variant_builder_close (&builder);
+
+               if (aptd_transaction (plugin, "UpgradePackages", app, NULL, g_variant_builder_end (&builder), 
error)) {
+                       gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+
+                       for (i = 0; i < apps->len; i++)
+                               gs_app_set_state (GS_APP (g_ptr_array_index (apps, i)), 
AS_APP_STATE_INSTALLED);
+
+                       unload_apt_db (plugin);
+
+                       gs_plugin_updates_changed (plugin);
+               } else {
+                       gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE);
+
+                       for (i = 0; i < apps->len; i++)
+                               gs_app_set_state (GS_APP (g_ptr_array_index (apps, i)), 
AS_APP_STATE_UPDATABLE_LIVE);
+
+                       return FALSE;
+               }
+       } else if (app_is_ours (app)) {
+               gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+
+               if (aptd_transaction (plugin, "UpgradePackages", app, NULL, NULL, error)) {
+                       gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+
+                       unload_apt_db (plugin);
+
+                       gs_plugin_updates_changed (plugin);
+               } else {
+                       gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE);
+
+                       return FALSE;
+               }
+       }
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_launch (GsPlugin *plugin,
+                 GsApp *app,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       if (!app_is_ours (app))
+               return TRUE;
+
+       return gs_plugin_app_launch (plugin, app, error);
+}
+
+/* vim: set noexpandtab ts=8 sw=8: */
diff --git a/src/plugins/gs-plugin-packagekit-refine.c b/src/plugins/gs-plugin-packagekit-refine.c
index d96ecd8..6c02bd6 100644
--- a/src/plugins/gs-plugin-packagekit-refine.c
+++ b/src/plugins/gs-plugin-packagekit-refine.c
@@ -341,21 +341,6 @@ gs_plugin_packagekit_refine_from_desktop (GsPlugin *plugin,
 static gchar *
 gs_plugin_packagekit_fixup_update_description (const gchar *text)
 {
-       gchar *tmp;
-       g_autoptr(GsMarkdown) markdown = NULL;
-
-       /* nothing to do */
-       if (text == NULL)
-               return NULL;
-
-       /* try to parse */
-       markdown = gs_markdown_new (GS_MARKDOWN_OUTPUT_TEXT);
-       gs_markdown_set_smart_quoting (markdown, FALSE);
-       gs_markdown_set_autocode (markdown, FALSE);
-       gs_markdown_set_autolinkify (markdown, FALSE);
-       tmp = gs_markdown_parse (markdown, text);
-       if (tmp != NULL)
-               return tmp;
        return g_strdup (text);
 }
 
diff --git a/src/plugins/gs-plugin-packagekit-refresh.c b/src/plugins/gs-plugin-packagekit-refresh.c
index 114e21d..6cf77d7 100644
--- a/src/plugins/gs-plugin-packagekit-refresh.c
+++ b/src/plugins/gs-plugin-packagekit-refresh.c
@@ -45,9 +45,6 @@ gs_plugin_initialize (GsPlugin *plugin)
        pk_task_set_only_download (priv->task, TRUE);
        pk_client_set_background (PK_CLIENT (priv->task), TRUE);
        pk_client_set_interactive (PK_CLIENT (priv->task), FALSE);
-
-       /* we can return better results than dpkg directly */
-       gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "dpkg");
 }
 
 void
diff --git a/src/plugins/gs-plugin-provenance.c b/src/plugins/gs-plugin-provenance.c
index e72779a..6cd7478 100644
--- a/src/plugins/gs-plugin-provenance.c
+++ b/src/plugins/gs-plugin-provenance.c
@@ -69,6 +69,7 @@ gs_plugin_initialize (GsPlugin *plugin)
        priv->sources = gs_plugin_provenance_get_sources (plugin);
 
        /* after the package source is set */
+       gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "apt");
        gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "dummy");
        gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "packagekit-refine");
 }
diff --git a/src/plugins/gs-plugin-snappy.c b/src/plugins/gs-plugin-snappy.c
new file mode 100644
index 0000000..e029c8c
--- /dev/null
+++ b/src/plugins/gs-plugin-snappy.c
@@ -0,0 +1,476 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <gs-plugin.h>
+#include <glib/gi18n.h>
+#include <json-glib/json-glib.h>
+#include <gnome-software.h>
+#include "gs-ubuntu-snapd.h"
+#include "gs-ubuntuone.h"
+
+// snapd API documentation is at https://github.com/ubuntu-core/snappy/blob/master/docs/rest.md
+
+#define SNAPD_SOCKET "/run/snapd.socket"
+
+typedef gboolean (*AppFilterFunc)(const gchar *id, JsonObject *object, gpointer data);
+
+void
+gs_plugin_initialize (GsPlugin *plugin)
+{
+       if (!g_file_test (SNAPD_SOCKET, G_FILE_TEST_EXISTS)) {
+               g_debug ("disabling '%s' as no %s available",
+                        gs_plugin_get_name (plugin), SNAPD_SOCKET);
+               gs_plugin_set_enabled (plugin, FALSE);
+       }
+}
+
+static JsonParser *
+parse_result (const gchar *response, const gchar *response_type, GError **error)
+{
+       g_autoptr(JsonParser) parser = NULL;
+       g_autoptr(GError) sub_error = NULL;
+
+       if (response_type == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "snapd returned no content type");
+               return NULL;
+       }
+       if (g_strcmp0 (response_type, "application/json") != 0) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "snapd returned unexpected content type %s", response_type);
+               return NULL;
+       }
+
+       parser = json_parser_new ();
+       if (!json_parser_load_from_data (parser, response, -1, &sub_error)) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Unable to parse snapd response: %s", sub_error->message);
+               return NULL;
+       }
+       if (!JSON_NODE_HOLDS_OBJECT (json_parser_get_root (parser))) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "snapd response does is not a valid JSON object");
+               return NULL;
+       }
+
+       return g_object_ref (parser);
+}
+
+static void
+refine_app (GsPlugin *plugin, GsApp *app, JsonObject *package)
+{
+       const gchar *status, *icon_url;
+       g_autoptr(GdkPixbuf) icon_pixbuf = NULL;
+       gint64 size = -1;
+
+       status = json_object_get_string_member (package, "status");
+       if (g_strcmp0 (status, "installed") == 0 || g_strcmp0 (status, "active") == 0) {
+               const gchar *update_available;
+
+               update_available = json_object_has_member (package, "update_available") ? 
json_object_get_string_member (package, "update_available") : NULL;
+               if (update_available)
+                       gs_app_set_state (app, AS_APP_STATE_UPDATABLE);
+               else
+                       gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+               size = json_object_get_int_member (package, "installed-size");
+       }
+       else if (g_strcmp0 (status, "removed") == 0) {
+               // A removed app is only available if it can be downloaded (it might have been sideloaded)
+               size = json_object_get_int_member (package, "download-size");
+               if (size > 0)
+                       gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+       } else if (g_strcmp0 (status, "not installed") == 0 || g_strcmp0 (status, "available") == 0) {
+               gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+               size = json_object_get_int_member (package, "download-size");
+       }
+       gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, json_object_get_string_member (package, "summary"));
+       gs_app_set_summary (app, GS_APP_QUALITY_HIGHEST, json_object_get_string_member (package, 
"description"));
+       gs_app_set_version (app, json_object_get_string_member (package, "version"));
+       gs_app_set_size_download (app, json_object_get_int_member (package, "download-size"));
+       gs_app_set_size_installed (app, json_object_get_int_member (package, "installed-size"));
+       gs_app_add_quirk (app, AS_APP_QUIRK_PROVENANCE);
+       icon_url = json_object_get_string_member (package, "icon");
+       if (g_str_has_prefix (icon_url, "/")) {
+               g_autofree gchar *icon_response = NULL;
+               gsize icon_response_length;
+
+               if (send_snapd_request ("GET", icon_url, NULL, TRUE, NULL, TRUE, NULL, NULL, NULL, NULL, 
&icon_response, &icon_response_length, NULL)) {
+                       g_autoptr(GdkPixbufLoader) loader = NULL;
+
+                       loader = gdk_pixbuf_loader_new ();
+                       gdk_pixbuf_loader_write (loader, (guchar *) icon_response, icon_response_length, 
NULL);
+                       gdk_pixbuf_loader_close (loader, NULL);
+                       icon_pixbuf = g_object_ref (gdk_pixbuf_loader_get_pixbuf (loader));
+               }
+               else
+                       g_printerr ("Failed to get icon\n");
+       }
+       else {
+               g_autoptr(SoupMessage) message = NULL;
+               g_autoptr(GdkPixbufLoader) loader = NULL;
+
+               message = soup_message_new (SOUP_METHOD_GET, icon_url);
+               if (message != NULL) {
+                       soup_session_send_message (gs_plugin_get_soup_session (plugin), message);
+                       loader = gdk_pixbuf_loader_new ();
+                       gdk_pixbuf_loader_write (loader, (guint8 *) message->response_body->data,  
message->response_body->length, NULL);
+                       gdk_pixbuf_loader_close (loader, NULL);
+                       icon_pixbuf = g_object_ref (gdk_pixbuf_loader_get_pixbuf (loader));
+               }
+       }
+
+       if (icon_pixbuf)
+               gs_app_set_pixbuf (app, icon_pixbuf);
+       else {
+               g_autoptr(AsIcon) icon = NULL;
+
+               icon = as_icon_new ();
+               as_icon_set_kind (icon, AS_ICON_KIND_STOCK);
+               as_icon_set_name (icon, "package-x-generic");
+               gs_app_add_icon (app, icon);
+       }
+}
+
+static gboolean
+get_apps (GsPlugin *plugin, const gchar *sources, gchar **search_terms, GsAppList *list, AppFilterFunc 
filter_func, gpointer user_data, GError **error)
+{
+       guint status_code;
+       GPtrArray *query_fields;
+       g_autoptr (GString) path = NULL;
+       g_autofree gchar *reason_phrase = NULL, *response_type = NULL, *response = NULL;
+       g_autoptr(JsonParser) parser = NULL;
+       JsonObject *root;
+       JsonArray *result;
+       GList *snaps;
+       GList *i;
+
+       /* Get all the apps */
+       query_fields = g_ptr_array_new_with_free_func (g_free);
+       if (sources != NULL)
+               g_ptr_array_add (query_fields, g_strdup_printf ("sources=%s", sources));
+       if (search_terms != NULL) {
+               g_autofree gchar *query = NULL;
+               query = g_strjoinv ("+", search_terms);
+               g_ptr_array_add (query_fields, g_strdup_printf ("q=%s", query));
+       }
+       g_ptr_array_add (query_fields, NULL);
+       path = g_string_new ("/v2/snaps");
+       if (query_fields->len > 1) {
+               g_autofree gchar *fields = NULL;
+               g_string_append (path, "?");
+               fields = g_strjoinv ("&", (gchar **) query_fields->pdata);
+               g_string_append (path, fields);
+       }
+       g_ptr_array_free (query_fields, TRUE);
+       if (!send_snapd_request ("GET", path->str, NULL, TRUE, NULL, TRUE, NULL, &status_code, 
&reason_phrase, &response_type, &response, NULL, error))
+               return FALSE;
+
+       if (status_code != SOUP_STATUS_OK) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "snapd returned status code %d: %s", status_code, reason_phrase);
+               return FALSE;
+       }
+
+       parser = parse_result (response, response_type, error);
+       if (parser == NULL)
+               return FALSE;
+
+       root = json_node_get_object (json_parser_get_root (parser));
+       result = json_object_get_array_member (root, "result");
+       snaps = json_array_get_elements (result);
+
+       for (i = snaps; i != NULL; i = i->next) {
+               JsonObject *package = json_node_get_object (i->data);
+               g_autoptr(GsApp) app = NULL;
+               const gchar *id;
+
+               id = json_object_get_string_member (package, "name");
+
+               if (filter_func != NULL && !filter_func (id, package, user_data))
+                       continue;
+
+               app = gs_app_new (id);
+               gs_app_set_management_plugin (app, "snappy");
+               gs_app_set_origin (app, _("Ubuntu Snappy Store"));
+               gs_app_set_kind (app, AS_APP_KIND_DESKTOP);
+               gs_app_add_quirk (app, AS_APP_QUIRK_NOT_REVIEWABLE);
+               gs_app_add_quirk (app, AS_APP_QUIRK_NOT_LAUNCHABLE);
+               refine_app (plugin, app, package);
+               gs_app_list_add (list, app);
+       }
+
+       g_list_free (snaps);
+
+       return TRUE;
+}
+
+static gboolean
+get_app (GsPlugin *plugin, GsApp *app, GError **error)
+{
+       guint status_code;
+       g_autofree gchar *path = NULL, *reason_phrase = NULL, *response_type = NULL, *response = NULL;
+       g_autoptr(JsonParser) parser = NULL;
+       JsonObject *root, *result;
+
+       path = g_strdup_printf ("/v2/snaps/%s", gs_app_get_id (app));
+       if (!send_snapd_request ("GET", path, NULL, TRUE, NULL, TRUE, NULL, &status_code, &reason_phrase, 
&response_type, &response, NULL, error))
+               return FALSE;
+
+       if (status_code != SOUP_STATUS_OK) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "snapd returned status code %d: %s", status_code, reason_phrase);
+               return FALSE;
+       }
+
+       parser = parse_result (response, response_type, error);
+       if (parser == NULL)
+               return FALSE;
+       root = json_node_get_object (json_parser_get_root (parser));
+       result = json_object_get_object_member (root, "result");
+       if (result == NULL) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "snapd returned no results for %s", gs_app_get_id (app));
+               return FALSE;
+       }
+
+       refine_app (plugin, app, result);
+
+       return TRUE;
+}
+
+void
+gs_plugin_destroy (GsPlugin *plugin)
+{
+}
+
+static gboolean
+is_active (const gchar *id, JsonObject *object, gpointer data)
+{
+       const gchar *status = json_object_get_string_member (object, "status");
+       return g_strcmp0 (status, "active") == 0;
+}
+
+gboolean
+gs_plugin_add_installed (GsPlugin *plugin,
+                        GsAppList *list,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       return get_apps (plugin, "local", NULL, list, is_active, NULL, error);
+}
+
+gboolean
+gs_plugin_add_search (GsPlugin *plugin,
+                     gchar **values,
+                     GsAppList *list,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       return get_apps (plugin, NULL, values, list, NULL, values, error);
+}
+
+gboolean
+gs_plugin_refine (GsPlugin *plugin,
+                 GsAppList *list,
+                 GsPluginRefineFlags flags,
+                 GCancellable *cancellable,
+                 GError **error)
+{
+       guint i;
+
+       for (i = 0; i < gs_app_list_length (list); i++) {
+               GsApp *app = gs_app_list_index (list, i);
+
+               if (g_strcmp0 (gs_app_get_management_plugin (app), "snappy") != 0)
+                       continue;
+
+               // Get info from snapd
+               if (!get_app (plugin, app, error))
+                       return FALSE;
+       }
+
+       return TRUE;
+}
+
+static gboolean
+send_package_action (GsPlugin *plugin, GsApp *app, const char *id, const gchar *action, GError **error)
+{
+       g_autofree gchar *content = NULL, *path = NULL;
+       guint status_code;
+       g_autofree gchar *reason_phrase = NULL, *response_type = NULL, *response = NULL, *status = NULL;
+       g_autoptr(JsonParser) parser = NULL;
+       JsonObject *root, *result, *task, *progress;
+       JsonArray *tasks;
+       GList *task_list, *l;
+       gint64 done, total, task_done, task_total;
+        const gchar *resource_path;
+       const gchar *type;
+       const gchar *change_id;
+       g_autoptr(GVariant) macaroon = NULL;
+
+       content = g_strdup_printf ("{\"action\": \"%s\"}", action);
+       path = g_strdup_printf ("/v2/snaps/%s", id);
+       if (!send_snapd_request ("POST", path, content, TRUE, NULL, TRUE, &macaroon, &status_code, 
&reason_phrase, &response_type, &response, NULL, error))
+               return FALSE;
+
+       if (status_code != SOUP_STATUS_ACCEPTED) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "snapd returned status code %d: %s", status_code, reason_phrase);
+               return FALSE;
+       }
+
+       parser = parse_result (response, response_type, error);
+       if (parser == NULL)
+               return FALSE;
+
+       root = json_node_get_object (json_parser_get_root (parser));
+       type = json_object_get_string_member (root, "type");
+
+       if (g_strcmp0 (type, "async") == 0) {
+               change_id = json_object_get_string_member (root, "change");
+               resource_path = g_strdup_printf ("/v2/changes/%s", change_id);
+
+               while (TRUE) {
+                       g_autofree gchar *status_reason_phrase = NULL, *status_response_type = NULL, 
*status_response = NULL;
+                       g_autoptr(JsonParser) status_parser = NULL;
+
+                       /* Wait for a little bit before polling */
+                       g_usleep (100 * 1000);
+
+                       if (!send_snapd_request ("GET", resource_path, NULL, TRUE, macaroon, TRUE, NULL,
+                                                &status_code, &status_reason_phrase, &status_response_type,
+                                                &status_response, NULL, error)) {
+                               return FALSE;
+                       }
+
+                       if (status_code != SOUP_STATUS_OK) {
+                               g_set_error (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_FAILED,
+                                            "snapd returned status code %d: %s", status_code, 
status_reason_phrase);
+                               return FALSE;
+                       }
+
+                       status_parser = parse_result (status_response, status_response_type, error);
+                       if (status_parser == NULL)
+                               return FALSE;
+
+                       root = json_node_get_object (json_parser_get_root (status_parser));
+                       result = json_object_get_object_member (root, "result");
+
+                       g_free (status);
+                       status = g_strdup (json_object_get_string_member (result, "status"));
+
+                       if (g_strcmp0 (status, "Done") == 0)
+                               break;
+
+                       tasks = json_object_get_array_member (result, "tasks");
+                       task_list = json_array_get_elements (tasks);
+
+                       done = 0;
+                       total = 0;
+
+                       for (l = task_list; l != NULL; l = l->next) {
+                               task = json_node_get_object (l->data);
+                               progress = json_object_get_object_member (task, "progress");
+                               task_done = json_object_get_int_member (progress, "done");
+                               task_total = json_object_get_int_member (progress, "total");
+
+                               done += task_done;
+                               total += task_total;
+                       }
+
+                       gs_app_set_progress (app, 100 * done / total);
+
+                       g_list_free (task_list);
+               }
+       }
+
+       if (g_strcmp0 (status, "Done") != 0) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "snapd operation finished with status %s", status);
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_app_install (GsPlugin *plugin,
+                      GsApp *app,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       gboolean result;
+
+       /* We can only install apps we know of */
+       if (g_strcmp0 (gs_app_get_management_plugin (app), "snappy") != 0)
+               return TRUE;
+
+       gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+       result = send_package_action (plugin, app, gs_app_get_id (app), "install", error);
+       if (result)
+               gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+       else
+               gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+
+       return result;
+}
+
+gboolean
+gs_plugin_app_remove (GsPlugin *plugin,
+                     GsApp *app,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       gboolean result;
+
+       /* We can only remove apps we know of */
+       if (g_strcmp0 (gs_app_get_management_plugin (app), "snappy") != 0)
+               return TRUE;
+
+       gs_app_set_state (app, AS_APP_STATE_REMOVING);
+       result = send_package_action (plugin, app, gs_app_get_id (app), "remove", error);
+       if (result)
+               gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+       else
+               gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+
+       return result;
+}
diff --git a/src/plugins/gs-plugin-ubuntu-reviews.c b/src/plugins/gs-plugin-ubuntu-reviews.c
index 6ad884a..c0046b4 100644
--- a/src/plugins/gs-plugin-ubuntu-reviews.c
+++ b/src/plugins/gs-plugin-ubuntu-reviews.c
@@ -24,13 +24,19 @@
 #include <string.h>
 #include <math.h>
 #include <json-glib/json-glib.h>
+#include <oauth.h>
 #include <sqlite3.h>
 #include <gnome-software.h>
+#include "gs-ubuntuone.h"
 
 struct GsPluginData {
        gchar           *db_path;
        sqlite3         *db;
        gsize            db_loaded;
+       gchar           *consumer_key;
+       gchar           *consumer_secret;
+       gchar           *token_key;
+       gchar           *token_secret;
 };
 
 typedef struct {
@@ -47,6 +53,9 @@ typedef struct {
 // FIXME: Much shorter time?
 #define REVIEW_STATS_AGE_MAX           (60 * 60 * 24 * 7 * 4 * 3)
 
+/* Number of pages of reviews to download */
+#define N_PAGES                                3
+
 void
 gs_plugin_initialize (GsPlugin *plugin)
 {
@@ -75,6 +84,11 @@ void
 gs_plugin_destroy (GsPlugin *plugin)
 {
        GsPluginData *priv = gs_plugin_get_data (plugin);
+
+       g_clear_pointer (&priv->token_secret, g_free);
+       g_clear_pointer (&priv->token_key, g_free);
+       g_clear_pointer (&priv->consumer_secret, g_free);
+       g_clear_pointer (&priv->consumer_key, g_free);
        g_clear_pointer (&priv->db, sqlite3_close);
        g_free (priv->db_path);
 }
@@ -332,9 +346,34 @@ parse_review_entries (GsPlugin *plugin, JsonParser *parser, GError **error)
        return TRUE;
 }
 
+static void
+sign_message (SoupMessage *message, OAuthMethod method,
+             const gchar *consumer_key, const gchar *consumer_secret,
+             const gchar *token_key, const gchar *token_secret)
+{
+       g_autofree gchar *url = NULL, *oauth_authorization_parameters = NULL, *authorization_text = NULL;
+       gchar **url_parameters = NULL;
+       int url_parameters_length;
+
+       url = soup_uri_to_string (soup_message_get_uri (message), FALSE);
+
+       url_parameters_length = oauth_split_url_parameters(url, &url_parameters);
+       oauth_sign_array2_process (&url_parameters_length, &url_parameters,
+                                  NULL,
+                                  method,
+                                  message->method,
+                                  consumer_key, consumer_secret,
+                                  token_key, token_secret);
+       oauth_authorization_parameters = oauth_serialize_url_sep (url_parameters_length, 1, url_parameters, 
", ", 6);
+       oauth_free_array (&url_parameters_length, &url_parameters);
+       authorization_text = g_strdup_printf ("OAuth realm=\"Ratings and Reviews\", %s", 
oauth_authorization_parameters);
+       soup_message_headers_append (message->request_headers, "Authorization", authorization_text);
+}
+
 static gboolean
-send_review_request (GsPlugin *plugin, const gchar *method, const gchar *path, JsonBuilder *request, 
JsonParser **result, GError **error)
+send_review_request (GsPlugin *plugin, const gchar *method, const gchar *path, JsonBuilder *request, 
gboolean do_sign, JsonParser **result, GError **error)
 {
+       GsPluginData *priv = gs_plugin_get_data (plugin);
        g_autofree gchar *uri = NULL;
        g_autoptr(SoupMessage) msg = NULL;
        guint status_code;
@@ -354,6 +393,14 @@ send_review_request (GsPlugin *plugin, const gchar *method, const gchar *path, J
                soup_message_set_request (msg, "application/json", SOUP_MEMORY_TAKE, data, length);
        }
 
+       if (do_sign)
+               sign_message (msg,
+                             OA_PLAINTEXT,
+                             priv->consumer_key,
+                             priv->consumer_secret,
+                             priv->token_key,
+                             priv->token_secret);
+
        status_code = soup_session_send_message (gs_plugin_get_soup_session (plugin), msg);
        if (status_code != SOUP_STATUS_OK) {
                g_set_error (error,
@@ -395,7 +442,7 @@ download_review_stats (GsPlugin *plugin, GError **error)
        g_autoptr(SoupMessage) msg = NULL;
        g_autoptr(JsonParser) result = NULL;
 
-       if (!send_review_request (plugin, SOUP_METHOD_GET, "/api/1.0/review-stats/any/any/", NULL, &result, 
error))
+       if (!send_review_request (plugin, SOUP_METHOD_GET, "/api/1.0/review-stats/any/any/", NULL, FALSE, 
&result, error))
                return FALSE;
 
        /* Extract the stats from the data */
@@ -543,8 +590,9 @@ parse_date_time (const gchar *text)
 }
 
 static GsReview *
-parse_review (JsonNode *node)
+parse_review (GsPlugin *plugin, JsonNode *node)
 {
+       GsPluginData *priv = gs_plugin_get_data (plugin);
        GsReview *review;
        JsonObject *object;
        gint64 star_rating;
@@ -556,6 +604,8 @@ parse_review (JsonNode *node)
        object = json_node_get_object (node);
 
        review = gs_review_new ();
+       if (g_strcmp0 (priv->consumer_key, json_object_get_string_member (object, "reviewer_username")) == 0)
+               gs_review_add_flags (review, GS_REVIEW_FLAG_SELF);
        gs_review_set_reviewer (review, json_object_get_string_member (object, "reviewer_displayname"));
        gs_review_set_summary (review, json_object_get_string_member (object, "summary"));
        gs_review_set_text (review, json_object_get_string_member (object, "review_text"));
@@ -583,7 +633,7 @@ parse_reviews (GsPlugin *plugin, JsonParser *parser, GsApp *app, GError **error)
                g_autoptr(GsReview) review = NULL;
 
                /* Read in from JSON... (skip bad entries) */
-               review = parse_review (json_array_get_element (array, i));
+               review = parse_review (plugin, json_array_get_element (array, i));
                if (review != NULL)
                        gs_app_add_review (app, review);
        }
@@ -606,7 +656,7 @@ get_language (GsPlugin *plugin)
 }
 
 static gboolean
-download_reviews (GsPlugin *plugin, GsApp *app, const gchar *package_name, GError **error)
+download_reviews (GsPlugin *plugin, GsApp *app, const gchar *package_name, gint page_number, GError **error)
 {
        g_autofree gchar *language = NULL, *path = NULL;
        g_autoptr(JsonParser) result = NULL;
@@ -614,8 +664,8 @@ download_reviews (GsPlugin *plugin, GsApp *app, const gchar *package_name, GErro
        /* Get the review stats using HTTP */
        // FIXME: This will only get the first page of reviews
        language = get_language (plugin);
-       path = g_strdup_printf ("/api/1.0/reviews/filter/%s/any/any/any/%s/", language, package_name);
-       if (!send_review_request (plugin, SOUP_METHOD_GET, path, NULL, &result, error))
+       path = g_strdup_printf ("/api/1.0/reviews/filter/%s/any/any/any/%s/page/%d/", language, package_name, 
page_number + 1);
+       if (!send_review_request (plugin, SOUP_METHOD_GET, path, NULL, FALSE, &result, error))
                return FALSE;
 
        /* Extract the stats from the data */
@@ -674,10 +724,44 @@ refine_rating (GsPlugin *plugin, GsApp *app, GError **error)
 }
 
 static gboolean
+get_ubuntuone_credentials (GsPlugin  *plugin,
+                          gboolean   required,
+                          GError   **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+
+       /* Use current credentials if already available */
+       if (priv->consumer_key != NULL &&
+           priv->consumer_secret != NULL &&
+           priv->token_key != NULL &&
+           priv->token_secret != NULL)
+               return TRUE;
+
+       /* Otherwise start with a clean slate */
+       g_clear_pointer (&priv->token_secret, g_free);
+       g_clear_pointer (&priv->token_key, g_free);
+       g_clear_pointer (&priv->consumer_secret, g_free);
+       g_clear_pointer (&priv->consumer_key, g_free);
+
+       /* Use credentials if we have them */
+       if (gs_ubuntuone_get_credentials (&priv->consumer_key, &priv->consumer_secret, &priv->token_key, 
&priv->token_secret))
+               return TRUE;
+
+       /* Otherwise log in to get them */
+       if (required)
+               return gs_ubuntuone_sign_in (&priv->consumer_key, &priv->consumer_secret, &priv->token_key, 
&priv->token_secret, error);
+       else
+               return TRUE;
+}
+
+static gboolean
 refine_reviews (GsPlugin *plugin, GsApp *app, GError **error)
 {
        GPtrArray *sources;
-       guint i;
+       guint i, j;
+
+       if (!get_ubuntuone_credentials (plugin, FALSE, error))
+               return FALSE;
 
        /* Skip if already has reviews */
        if (gs_app_get_reviews (app)->len > 0)
@@ -686,12 +770,15 @@ refine_reviews (GsPlugin *plugin, GsApp *app, GError **error)
        sources = gs_app_get_sources (app);
        for (i = 0; i < sources->len; i++) {
                const gchar *package_name;
-               gboolean ret;
 
                package_name = g_ptr_array_index (sources, i);
-               ret = download_reviews (plugin, app, package_name, error);
-               if (!ret)
-                       return FALSE;
+               for (j = 0; j < N_PAGES; j++) {
+                       gboolean ret;
+
+                       ret = download_reviews (plugin, app, package_name, j, error);
+                       if (!ret)
+                               return FALSE;
+               }
        }
 
        return TRUE;
@@ -716,3 +803,313 @@ gs_plugin_refine_app (GsPlugin *plugin,
        return TRUE;
 }
 
+static void
+add_string_member (JsonBuilder *builder, const gchar *name, const gchar *value)
+{
+       json_builder_set_member_name (builder, name);
+       json_builder_add_string_value (builder, value);
+}
+
+static void
+add_int_member (JsonBuilder *builder, const gchar *name, gint64 value)
+{
+       json_builder_set_member_name (builder, name);
+       json_builder_add_int_value (builder, value);
+}
+
+static gboolean
+set_package_review (GsPlugin *plugin,
+                   GsReview *review,
+                   const gchar *package_name,
+                   GError **error)
+{
+       gint rating;
+       gint n_stars;
+       g_autoptr(GsOsRelease) os_release = NULL;
+       g_autofree gchar *os_id = NULL, *os_ubuntu_codename = NULL, *language = NULL, *architecture = NULL;
+       g_autoptr(JsonBuilder) request = NULL;
+
+       /* Ubuntu reviews require a summary and description - just make one up for now */
+       rating = gs_review_get_rating (review);
+       if (rating > 80)
+               n_stars = 5;
+       else if (rating > 60)
+               n_stars = 4;
+       else if (rating > 40)
+               n_stars = 3;
+       else if (rating > 20)
+               n_stars = 2;
+       else
+               n_stars = 1;
+
+       os_release = gs_os_release_new (error);
+       if (os_release == NULL)
+               return FALSE;
+       os_id = gs_os_release_get_id (os_release);
+       if (os_id == NULL)
+               return FALSE;
+       os_ubuntu_codename = gs_os_release_get_ubuntu_codename (os_release);
+       if (os_ubuntu_codename == NULL)
+               return FALSE;
+
+       language = get_language (plugin);
+
+       // FIXME: Need to get Apt::Architecture configuration value from APT
+       architecture = g_strdup ("amd64");
+
+       /* Create message for reviews.ubuntu.com */
+       request = json_builder_new ();
+       json_builder_begin_object (request);
+       add_string_member (request, "package_name", package_name);
+       add_string_member (request, "summary", gs_review_get_summary (review));
+       add_string_member (request, "review_text", gs_review_get_text (review));
+       add_string_member (request, "language", language);
+       add_string_member (request, "origin", os_id);
+       add_string_member (request, "distroseries", os_ubuntu_codename);
+       add_string_member (request, "version", gs_review_get_version (review));
+       add_int_member (request, "rating", n_stars);
+       add_string_member (request, "arch_tag", architecture);
+       json_builder_end_object (request);
+
+       return send_review_request (plugin, SOUP_METHOD_POST, "/api/1.0/reviews/", request, TRUE, NULL, 
error);
+}
+
+static gboolean
+set_review_usefulness (GsPlugin *plugin,
+                      const gchar *review_id,
+                      gboolean is_useful,
+                      GError **error)
+{
+       g_autofree gchar *path = NULL;
+
+       if (!get_ubuntuone_credentials (plugin, TRUE, error))
+               return FALSE;
+
+       /* Create message for reviews.ubuntu.com */
+       path = g_strdup_printf ("/api/1.0/reviews/%s/recommendations/?useful=%s", review_id, is_useful ? 
"True" : "False");
+       return send_review_request (plugin, SOUP_METHOD_POST, path, NULL, TRUE, NULL, error);
+}
+
+static gboolean
+report_review (GsPlugin *plugin,
+              const gchar *review_id,
+              const gchar *reason,
+              const gchar *text,
+              GError **error)
+{
+       g_autofree gchar *path = NULL;
+
+       if (!get_ubuntuone_credentials (plugin, TRUE, error))
+               return FALSE;
+
+       /* Create message for reviews.ubuntu.com */
+       // FIXME: escape reason / text properly
+       path = g_strdup_printf ("/api/1.0/reviews/%s/recommendations/?reason=%s&text=%s", review_id, reason, 
text);
+       return send_review_request (plugin, SOUP_METHOD_POST, path, NULL, TRUE, NULL, error);
+}
+
+static gboolean
+remove_review (GsPlugin *plugin,
+              const gchar *review_id,
+              GError **error)
+{
+       g_autofree gchar *path = NULL;
+
+       if (!get_ubuntuone_credentials (plugin, TRUE, error))
+               return FALSE;
+
+       /* Create message for reviews.ubuntu.com */
+       path = g_strdup_printf ("/api/1.0/reviews/delete/%s/", review_id);
+       return send_review_request (plugin, SOUP_METHOD_POST, path, NULL, TRUE, NULL, error);
+}
+
+gboolean
+gs_plugin_review_submit (GsPlugin *plugin,
+                        GsApp *app,
+                        GsReview *review,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+
+       /* Load database once */
+       if (g_once_init_enter (&priv->db_loaded)) {
+               gboolean ret = load_database (plugin, error);
+               g_once_init_leave (&priv->db_loaded, TRUE);
+               if (!ret)
+                       return FALSE;
+       }
+
+       if (!get_ubuntuone_credentials (plugin, TRUE, error))
+               return FALSE;
+
+       return set_package_review (plugin,
+                                  review,
+                                  gs_app_get_source_default (app),
+                                  error);
+}
+
+gboolean
+gs_plugin_review_report (GsPlugin *plugin,
+                        GsApp *app,
+                        GsReview *review,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       const gchar *review_id;
+
+       /* Can only modify Ubuntu reviews */
+       review_id = gs_review_get_metadata_item (review, "ubuntu-id");
+       if (review_id == NULL)
+               return TRUE;
+
+       if (!report_review (plugin, review_id, "FIXME: gnome-software", "FIXME: gnome-software", error))
+               return FALSE;
+       gs_review_add_flags (review, GS_REVIEW_FLAG_VOTED);
+       return TRUE;
+}
+
+gboolean
+gs_plugin_review_upvote (GsPlugin *plugin,
+                        GsApp *app,
+                        GsReview *review,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       const gchar *review_id;
+
+       /* Can only modify Ubuntu reviews */
+       review_id = gs_review_get_metadata_item (review, "ubuntu-id");
+       if (review_id == NULL)
+               return TRUE;
+
+       if (!set_review_usefulness (plugin, review_id, TRUE, error))
+               return FALSE;
+       gs_review_add_flags (review, GS_REVIEW_FLAG_VOTED);
+       return TRUE;
+}
+
+gboolean
+gs_plugin_review_downvote (GsPlugin *plugin,
+                          GsApp *app,
+                          GsReview *review,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       const gchar *review_id;
+
+       /* Can only modify Ubuntu reviews */
+       review_id = gs_review_get_metadata_item (review, "ubuntu-id");
+       if (review_id == NULL)
+               return TRUE;
+
+       if (!set_review_usefulness (plugin, review_id, FALSE, error))
+               return FALSE;
+       gs_review_add_flags (review, GS_REVIEW_FLAG_VOTED);
+       return TRUE;
+}
+
+gboolean
+gs_plugin_review_remove (GsPlugin *plugin,
+                        GsApp *app,
+                        GsReview *review,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       const gchar *review_id;
+
+       /* Can only modify Ubuntu reviews */
+       review_id = gs_review_get_metadata_item (review, "ubuntu-id");
+       if (review_id == NULL)
+               return TRUE;
+
+       return remove_review (plugin, review_id, error);
+}
+
+typedef struct {
+       gchar           *package_name;
+       gint             rating;
+} PopularEntry;
+
+static gint
+popular_sqlite_cb (void *data,
+                  gint argc,
+                  gchar **argv,
+                  gchar **col_name)
+{
+       GList **list = data;
+       PopularEntry *entry;
+
+       entry = g_slice_new (PopularEntry);
+       entry->package_name = g_strdup (argv[0]);
+       entry->rating = get_rating (g_ascii_strtoll (argv[1], NULL, 10), g_ascii_strtoll (argv[2], NULL, 10), 
g_ascii_strtoll (argv[3], NULL, 10), g_ascii_strtoll (argv[4], NULL, 10), g_ascii_strtoll (argv[5], NULL, 
10));
+       *list = g_list_prepend (*list, entry);
+
+       return 0;
+}
+
+static gint
+compare_popular_entry (gconstpointer a, gconstpointer b)
+{
+       PopularEntry *ea = a, *eb = b;
+       return eb->rating - ea->rating;
+}
+
+static void
+free_popular_entry (gpointer data)
+{
+       PopularEntry *entry = data;
+       g_free (entry->package_name);
+       g_slice_free (PopularEntry, entry);
+}
+
+gboolean
+gs_plugin_add_popular (GsPlugin *plugin,
+                      GsAppList *list,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+       gint result;
+       GList *entries = NULL, *link;
+       char *error_msg = NULL;
+
+       /* Load database once */
+       if (g_once_init_enter (&priv->db_loaded)) {
+               gboolean ret = load_database (plugin, error);
+               g_once_init_leave (&priv->db_loaded, TRUE);
+               if (!ret)
+                       return FALSE;
+       }
+
+       result = sqlite3_exec (priv->db,
+                              "SELECT package_name, one_star_count, two_star_count, three_star_count, 
four_star_count, five_star_count FROM review_stats",
+                              popular_sqlite_cb,
+                              &entries,
+                              &error_msg);
+       if (result != SQLITE_OK) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "SQL error: %s", error_msg);
+               sqlite3_free (error_msg);
+               return FALSE;
+       }
+
+       entries = g_list_sort (entries, compare_popular_entry);
+       for (link = entries; link; link = link->next) {
+               PopularEntry *entry = link->data;
+               g_autoptr(GsApp) app = NULL;
+
+               /* Need four stars to show */
+               if (entry->rating < 80)
+                       break;
+
+               app = gs_app_new (NULL);
+               gs_app_add_source (app, entry->package_name);
+               gs_app_list_add (list, app);
+       }
+       g_list_free_full (entries, free_popular_entry);
+
+       return TRUE;
+}
diff --git a/src/plugins/gs-ubuntu-snapd.c b/src/plugins/gs-ubuntu-snapd.c
new file mode 100644
index 0000000..32fb2bc
--- /dev/null
+++ b/src/plugins/gs-ubuntu-snapd.c
@@ -0,0 +1,277 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <gs-plugin.h>
+#include <libsoup/soup.h>
+#include <gio/gunixsocketaddress.h>
+#include "gs-ubuntu-snapd.h"
+#include "gs-ubuntuone.h"
+
+#define SNAPD_SOCKET_PATH "/run/snapd.socket"
+
+// snapd API documentation is at https://github.com/ubuntu-core/snappy/blob/master/docs/rest.md
+
+static GSocket *
+open_snapd_socket (GError **error)
+{
+       GSocket *socket;
+       g_autoptr(GSocketAddress) address = NULL;
+       g_autoptr(GError) sub_error = NULL;
+
+       socket = g_socket_new (G_SOCKET_FAMILY_UNIX, G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_DEFAULT, 
&sub_error);
+       if (!socket) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Unable to open snapd socket: %s", sub_error->message);
+               return NULL;
+       }
+       address = g_unix_socket_address_new (SNAPD_SOCKET_PATH);
+       if (!g_socket_connect (socket, address, NULL, &sub_error)) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Unable to connect snapd socket: %s", sub_error->message);
+               g_object_unref (socket);
+               return NULL;
+       }
+
+       return socket;
+}
+
+static gboolean
+read_from_snapd (GSocket *socket, gchar *buffer, gsize buffer_length, gsize *read_offset, GError **error)
+{
+       gssize n_read;
+       n_read = g_socket_receive (socket, buffer + *read_offset, buffer_length - *read_offset, NULL, error);
+       if (n_read < 0)
+               return FALSE;
+       *read_offset += n_read;
+       buffer[*read_offset] = '\0';
+
+       return TRUE;
+}
+
+gboolean
+send_snapd_request (const gchar  *method,
+                   const gchar  *path,
+                   const gchar  *content,
+                   gboolean      authenticate,
+                   GVariant     *macaroon,
+                   gboolean      retry_after_login,
+                   GVariant    **out_macaroon,
+                   guint        *status_code,
+                   gchar       **reason_phrase,
+                   gchar       **response_type,
+                   gchar       **response,
+                   gsize        *response_length,
+                   GError      **error)
+{
+       g_autoptr (GSocket) socket = NULL;
+       g_autoptr (GString) request = NULL;
+       gssize n_written;
+       gsize max_data_length = 65535, data_length = 0, header_length;
+       gchar data[max_data_length + 1], *body = NULL;
+       g_autoptr (SoupMessageHeaders) headers = NULL;
+       g_autoptr(GVariant) auto_macaroon = NULL;
+       gsize chunk_length, n_required;
+       gchar *chunk_start = NULL;
+       const gchar *root;
+       const gchar *discharge;
+       GVariantIter *iter;
+       guint code;
+       gboolean ret;
+
+       if (macaroon == NULL && authenticate) {
+               auto_macaroon = gs_ubuntuone_get_macaroon (TRUE, FALSE, NULL);
+               macaroon = auto_macaroon;
+       }
+
+       // NOTE: Would love to use libsoup but it doesn't support unix sockets
+       // https://bugzilla.gnome.org/show_bug.cgi?id=727563
+
+       socket = open_snapd_socket (error);
+
+       if (socket == NULL)
+               return FALSE;
+
+       request = g_string_new ("");
+       g_string_append_printf (request, "%s %s HTTP/1.1\r\n", method, path);
+       g_string_append (request, "Host:\r\n");
+       if (macaroon != NULL) {
+               g_variant_get (macaroon, "(&sas)", &root, &iter);
+               g_string_append_printf (request, "Authorization: Macaroon root=\"%s\"", root);
+
+               while (g_variant_iter_next (iter, "&s", &discharge))
+                       g_string_append_printf (request, ",discharge=\"%s\"", discharge);
+
+               g_variant_iter_free (iter);
+               g_string_append (request, "\r\n");
+       }
+       if (content)
+               g_string_append_printf (request, "Content-Length: %zi\r\n", strlen (content));
+       g_string_append (request, "\r\n");
+       if (content)
+               g_string_append (request, content);
+
+       if (g_strcmp0 (g_getenv ("GNOME_SOFTWARE_SNAPPY"), "debug") == 0)
+               g_print ("===== begin snapd request =====\n%s\n===== end snapd request =====\n", 
request->str);
+
+       /* Send HTTP request */
+       n_written = g_socket_send (socket, request->str, request->len, NULL, error);
+       if (n_written < 0)
+               return FALSE;
+
+       /* Read HTTP headers */
+       while (data_length < max_data_length && !body) {
+               if (!read_from_snapd (socket, data, max_data_length, &data_length, error))
+                       return FALSE;
+               body = strstr (data, "\r\n\r\n");
+       }
+       if (!body) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "Unable to find header separator in snapd response");
+               return FALSE;
+       }
+
+       /* Body starts after header divider */
+       body += 4;
+       header_length = body - data;
+
+       /* Parse headers */
+       headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
+       if (!soup_headers_parse_response (data, header_length, headers, NULL, &code, reason_phrase)) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "snapd response HTTP headers not parseable");
+               return FALSE;
+       }
+
+       if (status_code != NULL)
+               *status_code = code;
+
+       if ((code == 401 || code == 403) && retry_after_login) {
+               g_socket_close (socket, NULL);
+
+               gs_ubuntuone_clear_macaroon ();
+
+               macaroon = gs_ubuntuone_get_macaroon (FALSE, TRUE, NULL);
+
+               if (macaroon == NULL) {
+                       g_set_error_literal (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
+                                            "failed to authenticate");
+                       return FALSE;
+               }
+
+               ret = send_snapd_request (method,
+                                         path,
+                                         content,
+                                         TRUE,
+                                         macaroon,
+                                         FALSE,
+                                         NULL,
+                                         status_code,
+                                         reason_phrase,
+                                         response_type,
+                                         response,
+                                         response_length,
+                                         error);
+
+               if (ret && out_macaroon != NULL) {
+                       *out_macaroon = macaroon;
+               } else {
+                       g_variant_unref (macaroon);
+               }
+
+               return ret;
+       }
+
+       /* Work out how much data to follow */
+       if (g_strcmp0 (soup_message_headers_get_one (headers, "Transfer-Encoding"), "chunked") == 0) {
+               while (data_length < max_data_length) {
+                       chunk_start = strstr (body, "\r\n");
+                       if (chunk_start)
+                               break;
+                       if (!read_from_snapd (socket, data, max_data_length, &data_length, error))
+                               return FALSE;
+               }
+               if (!chunk_start) {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_FAILED,
+                                            "Unable to find chunk header in snapd response");
+                       return FALSE;
+               }
+               chunk_length = strtoul (body, NULL, 16);
+               chunk_start += 2;
+               // FIXME: Support multiple chunks
+       }
+       else {
+               const gchar *value;
+               value = soup_message_headers_get_one (headers, "Content-Length");
+               if (!value) {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_FAILED,
+                                            "Unable to determine content length of snapd response");
+                       return FALSE;
+               }
+               chunk_length = strtoul (value, NULL, 10);
+               chunk_start = body;
+       }
+
+       /* Check if enough space to read chunk */
+       n_required = (chunk_start - data) + chunk_length;
+       if (n_required > max_data_length) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Not enough space for snapd response, require %zi octets, have %zi", n_required, 
max_data_length);
+               return FALSE;
+       }
+
+       /* Read chunk content */
+       while (data_length < n_required)
+               if (!read_from_snapd (socket, data, n_required - data_length, &data_length, error))
+                       return FALSE;
+
+       if (out_macaroon != NULL)
+               *out_macaroon = g_variant_ref (macaroon);
+       if (response_type)
+               *response_type = g_strdup (soup_message_headers_get_one (headers, "Content-Type"));
+       if (response) {
+               *response = g_malloc (chunk_length + 2);
+               memcpy (*response, chunk_start, chunk_length + 1);
+               (*response)[chunk_length + 1] = '\0';
+
+               if (g_strcmp0 (g_getenv ("GNOME_SOFTWARE_SNAPPY"), "debug") == 0)
+                       g_print ("===== begin snapd response =====\nStatus %u\n%s\n===== end snapd response 
=====\n", code, *response);
+       }
+       if (response_length)
+               *response_length = chunk_length;
+
+       return TRUE;
+}
diff --git a/src/plugins/gs-ubuntu-snapd.h b/src/plugins/gs-ubuntu-snapd.h
new file mode 100644
index 0000000..cbc1e1a
--- /dev/null
+++ b/src/plugins/gs-ubuntu-snapd.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_UBUNTU_SNAPD_H__
+#define __GS_UBUNTU_SNAPD_H__
+
+#include <gio/gio.h>
+
+gboolean send_snapd_request (const gchar  *method,
+                            const gchar  *path,
+                            const gchar  *content,
+                            gboolean      authenticate,
+                            GVariant     *macaroon,
+                            gboolean      retry_after_login,
+                            GVariant    **out_macaroon,
+                            guint        *status_code,
+                            gchar       **reason_phrase,
+                            gchar       **response_type,
+                            gchar       **response,
+                            gsize        *response_length,
+                            GError      **error);
+
+#endif /* __GS_UBUNTU_SNAPD_H__ */
diff --git a/src/plugins/gs-ubuntuone-dialog.c b/src/plugins/gs-ubuntuone-dialog.c
new file mode 100644
index 0000000..7d982a7
--- /dev/null
+++ b/src/plugins/gs-ubuntuone-dialog.c
@@ -0,0 +1,666 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "gs-ubuntuone-dialog.h"
+#include "gs-common.h"
+#include "gs-utils.h"
+
+#include <glib/gi18n.h>
+#include <json-glib/json-glib.h>
+#include <libsoup/soup.h>
+
+#include "gs-ubuntu-snapd.h"
+
+#define UBUNTU_LOGIN_HOST "https://login.ubuntu.com";
+
+struct _GsUbuntuoneDialog
+{
+       GtkDialog parent_instance;
+
+       GtkWidget *content_box;
+       GtkWidget *cancel_button;
+       GtkWidget *next_button;
+       GtkWidget *status_stack;
+       GtkWidget *status_image;
+       GtkWidget *status_label;
+       GtkWidget *page_stack;
+       GtkWidget *prompt_label;
+       GtkWidget *login_radio;
+       GtkWidget *register_radio;
+       GtkWidget *reset_radio;
+       GtkWidget *email_entry;
+       GtkWidget *password_entry;
+       GtkWidget *remember_check;
+       GtkWidget *passcode_entry;
+
+       SoupSession *session;
+
+       gboolean get_macaroon;
+
+       GVariant *macaroon;
+       gchar *consumer_key;
+       gchar *consumer_secret;
+       gchar *token_key;
+       gchar *token_secret;
+};
+
+G_DEFINE_TYPE (GsUbuntuoneDialog, gs_ubuntuone_dialog, GTK_TYPE_DIALOG)
+
+static gboolean
+is_email_address (const gchar *text)
+{
+       text = g_utf8_strchr (text, -1, '@');
+
+       if (!text)
+               return FALSE;
+
+       text = g_utf8_strchr (text + 1, -1, '.');
+
+       if (!text)
+               return FALSE;
+
+       return text[1];
+}
+
+static void
+update_widgets (GsUbuntuoneDialog *self)
+{
+       if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-0")) {
+               gtk_widget_set_sensitive (self->next_button,
+                                         !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON 
(self->login_radio)) ||
+                                         (is_email_address (gtk_entry_get_text (GTK_ENTRY 
(self->email_entry))) &&
+                                          gtk_entry_get_text_length (GTK_ENTRY (self->password_entry)) > 0));
+               gtk_widget_set_sensitive (self->password_entry,
+                                         gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON 
(self->login_radio)));
+               gtk_widget_set_sensitive (self->remember_check,
+                                         gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON 
(self->login_radio)));
+       } else if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-1")) {
+               gtk_widget_set_sensitive (self->next_button, gtk_entry_get_text_length (GTK_ENTRY 
(self->passcode_entry)) > 0);
+       } else if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-2")) {
+               gtk_widget_set_visible (self->cancel_button, FALSE);
+               gtk_widget_set_sensitive (self->cancel_button, FALSE);
+               gtk_button_set_label (GTK_BUTTON (self->next_button), _("_Continue"));
+       }
+}
+
+typedef void (*ResponseCallback) (GsUbuntuoneDialog *self,
+                                 guint       status,
+                                 GVariant        *response,
+                                 gpointer         user_data);
+
+typedef struct
+{
+       GsUbuntuoneDialog *dialog;
+       ResponseCallback callback;
+       gpointer user_data;
+} RequestInfo;
+
+static void
+response_received_cb (SoupSession *session,
+                     SoupMessage *message,
+                     gpointer     user_data)
+{
+       RequestInfo *info = user_data;
+       g_autoptr(GVariant) response = NULL;
+       guint status;
+       GBytes *bytes;
+       g_autofree gchar *body = NULL;
+       gsize length;
+
+       g_object_get (message,
+                     SOUP_MESSAGE_STATUS_CODE, &status,
+                     SOUP_MESSAGE_RESPONSE_BODY_DATA, &bytes,
+                     NULL);
+
+       body = g_bytes_unref_to_data (bytes, &length);
+
+       if (body)
+               response = json_gvariant_deserialize_data (body, length, NULL, NULL);
+
+       if (response)
+               g_variant_ref_sink (response);
+
+       if (info->callback)
+               info->callback (info->dialog, status, response, info->user_data);
+
+       g_free (info);
+}
+
+static void
+send_request (GsUbuntuoneDialog *self,
+             const gchar         *method,
+             const gchar         *uri,
+             GVariant            *request,
+             ResponseCallback     callback,
+             gpointer             user_data)
+{
+       RequestInfo *info;
+       SoupMessage *message;
+       gchar *body;
+       gsize length;
+       g_autofree gchar *url = NULL;
+
+       if (self->session == NULL)
+               self->session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT,
+                                                              gs_user_agent (),
+                                                              NULL);
+
+       body = json_gvariant_serialize_data (g_variant_ref_sink (request), &length);
+       g_variant_unref (request);
+
+       url = g_strdup_printf ("%s%s", UBUNTU_LOGIN_HOST, uri);
+       message = soup_message_new (method, url);
+
+       info = g_new0 (RequestInfo, 1);
+       info->dialog = self;
+       info->callback = callback;
+       info->user_data = user_data;
+
+       soup_message_set_request (message, "application/json", SOUP_MEMORY_TAKE, body, length);
+       soup_session_queue_message (self->session, message, response_received_cb, info);
+}
+
+static void
+show_status (GsUbuntuoneDialog *self,
+            const gchar       *text,
+            gboolean           is_error)
+{
+       PangoAttrList *attributes;
+
+       gtk_widget_set_visible (self->status_stack, TRUE);
+
+       if (is_error) {
+               gtk_stack_set_visible_child_name (GTK_STACK (self->status_stack), "status-image");
+               gtk_image_set_from_icon_name (GTK_IMAGE (self->status_image), "gtk-dialog-error", 
GTK_ICON_SIZE_BUTTON);
+       } else {
+               gtk_stack_set_visible_child_name (GTK_STACK (self->status_stack), "status-spinner");
+       }
+
+       attributes = pango_attr_list_new ();
+       pango_attr_list_insert (attributes, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+       pango_attr_list_insert (attributes, pango_attr_foreground_new (is_error ? 65535 : 0, 0, 0));
+       gtk_label_set_attributes (GTK_LABEL (self->status_label), attributes);
+       pango_attr_list_unref (attributes);
+
+       gtk_label_set_text (GTK_LABEL (self->status_label), text);
+}
+
+static void
+reenable_widgets (GsUbuntuoneDialog *self)
+{
+       gtk_label_set_text (GTK_LABEL (self->status_label), NULL);
+       gtk_stack_set_visible_child_name (GTK_STACK (self->status_stack), "status-image");
+       gtk_widget_set_visible (self->status_stack, FALSE);
+
+       gtk_widget_set_sensitive (self->cancel_button, TRUE);
+       gtk_widget_set_sensitive (self->next_button, TRUE);
+       gtk_widget_set_sensitive (self->login_radio, TRUE);
+       gtk_widget_set_sensitive (self->register_radio, TRUE);
+       gtk_widget_set_sensitive (self->reset_radio, TRUE);
+       gtk_widget_set_sensitive (self->email_entry, TRUE);
+       gtk_widget_set_sensitive (self->password_entry, TRUE);
+       gtk_widget_set_sensitive (self->remember_check, TRUE);
+       gtk_widget_set_sensitive (self->passcode_entry, TRUE);
+}
+
+static void
+receive_login_response_cb (GsUbuntuoneDialog *self,
+                          guint                status,
+                          GVariant            *response,
+                          gpointer             user_data)
+{
+       const gchar *code;
+
+       reenable_widgets (self);
+
+       if (response) {
+               switch (status) {
+               case SOUP_STATUS_OK:
+               case SOUP_STATUS_CREATED:
+                       g_clear_pointer (&self->token_secret, g_free);
+                       g_clear_pointer (&self->token_key, g_free);
+                       g_clear_pointer (&self->consumer_secret, g_free);
+                       g_clear_pointer (&self->consumer_key, g_free);
+
+                       g_variant_lookup (response, "consumer_key", "s", &self->consumer_key);
+                       g_variant_lookup (response, "consumer_secret", "s", &self->consumer_secret);
+                       g_variant_lookup (response, "token_key", "s", &self->token_key);
+                       g_variant_lookup (response, "token_secret", "s", &self->token_secret);
+
+                       gtk_stack_set_visible_child_name (GTK_STACK (self->page_stack), "page-2");
+                       update_widgets (self);
+                       break;
+
+               default:
+                       g_variant_lookup (response, "code", "&s", &code);
+
+                       if (!code)
+                               code = "";
+
+                       if (g_str_equal (code, "TWOFACTOR_REQUIRED")) {
+                               gtk_stack_set_visible_child_name (GTK_STACK (self->page_stack), "page-1");
+                               gtk_widget_grab_focus (self->passcode_entry);
+                               update_widgets (self);
+                               break;
+                       }
+
+                       update_widgets (self);
+
+                       if (g_str_equal (code, "INVALID_CREDENTIALS")) {
+                               show_status (self, _("Incorrect email or password"), TRUE);
+                               gtk_widget_grab_focus (self->password_entry);
+                       } else if (g_str_equal (code, "ACCOUNT_SUSPENDED")) {
+                               show_status (self, _("Account suspended"), TRUE);
+                               gtk_widget_grab_focus (self->email_entry);
+                       } else if (g_str_equal (code, "ACCOUNT_DEACTIVATED")) {
+                               show_status (self, _("Account deactivated"), TRUE);
+                               gtk_widget_grab_focus (self->email_entry);
+                       } else if (g_str_equal (code, "EMAIL_INVALIDATED")) {
+                               show_status (self, _("Email invalidated"), TRUE);
+                               gtk_widget_grab_focus (self->email_entry);
+                       } else if (g_str_equal (code, "TWOFACTOR_FAILURE")) {
+                               show_status (self, _("Two-factor authentication failed"), TRUE);
+                               gtk_widget_grab_focus (self->passcode_entry);
+                       } else if (g_str_equal (code, "PASSWORD_POLICY_ERROR")) {
+                               show_status (self, _("Password reset required"), TRUE);
+                               gtk_widget_grab_focus (self->reset_radio);
+                       } else if (g_str_equal (code, "TOO_MANY_REQUESTS")) {
+                               show_status (self, _("Too many requests"), TRUE);
+                               gtk_widget_grab_focus (self->password_entry);
+                       } else {
+                               show_status (self, _("An error occurred"), TRUE);
+                               gtk_widget_grab_focus (self->password_entry);
+                       }
+
+                       break;
+               }
+       } else {
+               update_widgets (self);
+               show_status (self, _("An error occurred"), TRUE);
+               gtk_widget_grab_focus (self->password_entry);
+       }
+}
+
+static void
+check_snapd_response (GsUbuntuoneDialog *self,
+                     guint              status_code,
+                     gchar             *reason_phrase,
+                     gchar             *response_type,
+                     gchar             *response,
+                     gsize              response_length)
+{
+       g_autoptr(GVariant) variant = NULL;
+       g_autoptr(GVariant) result = NULL;
+       g_autoptr(GVariant) discharges = NULL;
+       const gchar *type;
+       const gchar *kind;
+       const gchar *macaroon;
+       GVariantBuilder builder;
+       GVariantIter iter;
+       GVariant *discharge = NULL;
+
+       if (status_code == 401) {
+               /* snapd isn't giving us enough information to tell why the authentication failed... */
+               if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-1")) {
+                       show_status (self, _("Two-factor authentication failed"), TRUE);
+                       gtk_widget_grab_focus (self->passcode_entry);
+                       return;
+               }
+       }
+
+       variant = json_gvariant_deserialize_data (response, -1, NULL, NULL);
+
+       if (variant == NULL)
+               goto err;
+
+       g_variant_ref_sink (variant);
+
+       if (!g_variant_lookup (variant, "type", "&s", &type))
+               goto err;
+
+       result = g_variant_lookup_value (variant, "result", G_VARIANT_TYPE_DICTIONARY);
+
+       if (result == NULL)
+               goto err;
+
+       if (g_str_equal (type, "sync")) {
+               if (!g_variant_lookup (result, "macaroon", "&s", &macaroon))
+                       goto err;
+
+               discharges = g_variant_lookup_value (result, "discharges", G_VARIANT_TYPE ("av"));
+
+               if (discharges == NULL)
+                       goto err;
+
+               g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
+               g_variant_iter_init (&iter, discharges);
+
+               while (g_variant_iter_loop (&iter, "v", &discharge))
+                       g_variant_builder_add (&builder, "s", g_variant_get_string (discharge, NULL));
+
+               self->macaroon = g_variant_ref_sink (g_variant_new ("(s as)", macaroon, g_variant_builder_end 
(&builder)));
+
+               gtk_stack_set_visible_child_name (GTK_STACK (self->page_stack), "page-2");
+               update_widgets (self);
+       } else if (g_variant_lookup (result, "kind", "&s", &kind)) {
+               if (g_str_equal (kind, "two-factor-required")) {
+                       gtk_stack_set_visible_child_name (GTK_STACK (self->page_stack), "page-1");
+                       gtk_widget_grab_focus (self->passcode_entry);
+                       update_widgets (self);
+               } else
+                       goto err;
+       } else
+               goto err;
+
+       return;
+
+err:
+       /* snapd isn't giving us enough information to tell why the authentication failed... */
+       show_status (self, status_code == 401 ? _("Incorrect email or password") : _("An error occurred"), 
TRUE);
+       gtk_widget_grab_focus (self->password_entry);
+}
+
+static void
+send_login_request (GsUbuntuoneDialog *self)
+{
+       g_autofree gchar *content = NULL;
+       g_autofree gchar *username = NULL;
+       g_autofree gchar *password = NULL;
+       g_autofree gchar *otp = NULL;
+       GVariant *request;
+       guint status_code;
+       g_autofree gchar *reason_phrase = NULL;
+       g_autofree gchar *response_type = NULL;
+       g_autofree gchar *response = NULL;
+       gsize response_length;
+       g_autoptr(GError) error = NULL;
+
+       gtk_widget_set_sensitive (self->cancel_button, FALSE);
+       gtk_widget_set_sensitive (self->next_button, FALSE);
+       gtk_widget_set_sensitive (self->login_radio, FALSE);
+       gtk_widget_set_sensitive (self->register_radio, FALSE);
+       gtk_widget_set_sensitive (self->reset_radio, FALSE);
+       gtk_widget_set_sensitive (self->email_entry, FALSE);
+       gtk_widget_set_sensitive (self->password_entry, FALSE);
+       gtk_widget_set_sensitive (self->remember_check, FALSE);
+       gtk_widget_set_sensitive (self->passcode_entry, FALSE);
+
+       show_status (self, _("Signing in…"), FALSE);
+
+       if (self->get_macaroon) {
+               username = g_strescape (gtk_entry_get_text (GTK_ENTRY (self->email_entry)), NULL);
+               password = g_strescape (gtk_entry_get_text (GTK_ENTRY (self->password_entry)), NULL);
+
+               if (gtk_entry_get_text_length (GTK_ENTRY (self->passcode_entry)) > 0) {
+                       otp = g_strescape (gtk_entry_get_text (GTK_ENTRY (self->passcode_entry)), NULL);
+
+                       content = g_strdup_printf ("{"
+                                                  "  \"username\" : \"%s\","
+                                                  "  \"password\" : \"%s\","
+                                                  "  \"otp\" : \"%s\""
+                                                  "}",
+                                                  username,
+                                                  password,
+                                                  otp);
+               } else {
+                       content = g_strdup_printf ("{"
+                                                  "  \"username\" : \"%s\","
+                                                  "  \"password\" : \"%s\""
+                                                  "}",
+                                                  username,
+                                                  password);
+               }
+
+               if (send_snapd_request (SOUP_METHOD_POST,
+                                       "/v2/login",
+                                       content,
+                                       FALSE,
+                                       NULL,
+                                       FALSE,
+                                       NULL,
+                                       &status_code,
+                                       &reason_phrase,
+                                       &response_type,
+                                       &response,
+                                       &response_length,
+                                       &error)) {
+                       reenable_widgets (self);
+
+                       check_snapd_response (self,
+                                             status_code,
+                                             reason_phrase,
+                                             response_type,
+                                             response,
+                                             response_length);
+               } else {
+                       g_warning ("could not send request: %s", error->message);
+
+                       reenable_widgets (self);
+                       show_status (self, _("An error occurred"), TRUE);
+                       gtk_widget_grab_focus (self->password_entry);
+               }
+       } else {
+               if (gtk_entry_get_text_length (GTK_ENTRY (self->passcode_entry)) > 0) {
+                       request = g_variant_new_parsed ("{"
+                                                       "  'token_name' : <'GNOME Software'>,"
+                                                       "  'email' : <%s>,"
+                                                       "  'password' : <%s>,"
+                                                       "  'otp' : <%s>"
+                                                       "}",
+                                                       gtk_entry_get_text (GTK_ENTRY (self->email_entry)),
+                                                       gtk_entry_get_text (GTK_ENTRY (self->password_entry)),
+                                                       gtk_entry_get_text (GTK_ENTRY 
(self->passcode_entry)));
+               } else {
+                       request = g_variant_new_parsed ("{"
+                                                       "  'token_name' : <'GNOME Software'>,"
+                                                       "  'email' : <%s>,"
+                                                       "  'password' : <%s>"
+                                                       "}",
+                                                       gtk_entry_get_text (GTK_ENTRY (self->email_entry)),
+                                                       gtk_entry_get_text (GTK_ENTRY 
(self->password_entry)));
+               }
+
+               send_request (self,
+                             SOUP_METHOD_POST,
+                             "/api/v2/tokens/oauth",
+                             request,
+                             receive_login_response_cb,
+                             NULL);
+       }
+}
+
+static void
+next_button_clicked_cb (GsUbuntuoneDialog *self,
+                       GtkButton           *button)
+{
+       if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-0")) {
+               if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->login_radio))) {
+                       send_login_request (self);
+               } else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->register_radio))) {
+                       g_app_info_launch_default_for_uri ("https://login.ubuntu.com/+new_account";, NULL, 
NULL);
+               } else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->reset_radio))) {
+                       g_app_info_launch_default_for_uri ("https://login.ubuntu.com/+forgot_password";, NULL, 
NULL);
+               }
+       } else if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-1")) {
+               send_login_request (self);
+       } else if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-2")) {
+               gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_OK);
+       }
+}
+
+static void
+radio_button_toggled_cb (GsUbuntuoneDialog *self,
+                        GtkToggleButton   *toggle)
+{
+       update_widgets (self);
+}
+
+static void
+entry_edited_cb (GsUbuntuoneDialog *self,
+                GParamSpec          *pspec,
+                GObject             *object)
+{
+       update_widgets (self);
+}
+
+static void
+gs_ubuntuone_dialog_init (GsUbuntuoneDialog *self)
+{
+       GList *focus_chain = NULL;
+
+       gtk_widget_init_template (GTK_WIDGET (self));
+
+       gtk_window_set_default (GTK_WINDOW (self), self->next_button);
+
+       focus_chain = g_list_append (focus_chain, self->email_entry);
+       focus_chain = g_list_append (focus_chain, self->password_entry);
+       focus_chain = g_list_append (focus_chain, self->remember_check);
+       focus_chain = g_list_append (focus_chain, self->login_radio);
+       focus_chain = g_list_append (focus_chain, self->register_radio);
+       focus_chain = g_list_append (focus_chain, self->reset_radio);
+       gtk_container_set_focus_chain (GTK_CONTAINER (gtk_widget_get_parent (self->email_entry)), 
focus_chain);
+       g_list_free (focus_chain);
+
+       g_signal_connect_swapped (self->next_button, "clicked", G_CALLBACK (next_button_clicked_cb), self);
+       g_signal_connect_swapped (self->login_radio, "toggled", G_CALLBACK (radio_button_toggled_cb), self);
+       g_signal_connect_swapped (self->register_radio, "toggled", G_CALLBACK (radio_button_toggled_cb), 
self);
+       g_signal_connect_swapped (self->reset_radio, "toggled", G_CALLBACK (radio_button_toggled_cb), self);
+       g_signal_connect_swapped (self->email_entry, "notify::text", G_CALLBACK (entry_edited_cb), self);
+       g_signal_connect_swapped (self->password_entry, "notify::text", G_CALLBACK (entry_edited_cb), self);
+       g_signal_connect_swapped (self->passcode_entry, "notify::text", G_CALLBACK (entry_edited_cb), self);
+
+       update_widgets (self);
+}
+
+static void
+gs_ubuntuone_dialog_dispose (GObject *object)
+{
+       GsUbuntuoneDialog *self = GS_UBUNTUONE_DIALOG (object);
+
+       g_clear_object (&self->session);
+
+       G_OBJECT_CLASS (gs_ubuntuone_dialog_parent_class)->dispose (object);
+}
+
+static void
+gs_ubuntuone_dialog_finalize (GObject *object)
+{
+       GsUbuntuoneDialog *self = GS_UBUNTUONE_DIALOG (object);
+
+       g_clear_pointer (&self->token_secret, g_free);
+       g_clear_pointer (&self->token_key, g_free);
+       g_clear_pointer (&self->consumer_secret, g_free);
+       g_clear_pointer (&self->consumer_key, g_free);
+       g_clear_pointer (&self->macaroon, g_variant_unref);
+
+       G_OBJECT_CLASS (gs_ubuntuone_dialog_parent_class)->finalize (object);
+}
+
+static void
+gs_ubuntuone_dialog_class_init (GsUbuntuoneDialogClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+       object_class->dispose = gs_ubuntuone_dialog_dispose;
+       object_class->finalize = gs_ubuntuone_dialog_finalize;
+
+       gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/Software/plugins/gs-ubuntuone-dialog.ui");
+
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, content_box);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, cancel_button);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, next_button);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, status_stack);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, status_image);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, status_label);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, page_stack);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, prompt_label);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, login_radio);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, register_radio);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, reset_radio);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, email_entry);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, password_entry);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, remember_check);
+       gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, passcode_entry);
+}
+
+gboolean
+gs_ubuntuone_dialog_get_do_remember (GsUbuntuoneDialog *dialog)
+{
+       g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), FALSE);
+       return gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dialog->remember_check));
+}
+
+GVariant *
+gs_ubuntuone_dialog_get_macaroon (GsUbuntuoneDialog *dialog)
+{
+       g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), NULL);
+       return dialog->macaroon;
+}
+
+const gchar *
+gs_ubuntuone_dialog_get_consumer_key (GsUbuntuoneDialog *dialog)
+{
+       g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), NULL);
+       return dialog->consumer_key;
+}
+
+const gchar *
+gs_ubuntuone_dialog_get_consumer_secret (GsUbuntuoneDialog *dialog)
+{
+       g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), NULL);
+       return dialog->consumer_secret;
+}
+
+const gchar *
+gs_ubuntuone_dialog_get_token_key (GsUbuntuoneDialog *dialog)
+{
+       g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), NULL);
+       return dialog->token_key;
+}
+
+const gchar *
+gs_ubuntuone_dialog_get_token_secret (GsUbuntuoneDialog *dialog)
+{
+       g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), NULL);
+       return dialog->token_secret;
+}
+
+GtkWidget *
+gs_ubuntuone_dialog_new (gboolean get_macaroon)
+{
+       GsUbuntuoneDialog *dialog = g_object_new (GS_TYPE_UBUNTUONE_DIALOG,
+                                                 "use-header-bar", TRUE,
+                                                 NULL);
+
+       dialog->get_macaroon = get_macaroon;
+
+       if (dialog->get_macaroon)
+               gtk_label_set_label (GTK_LABEL (dialog->prompt_label),
+                       _("To install and remove snaps, you need an Ubuntu Single Sign-On account."));
+       else
+               gtk_label_set_label (GTK_LABEL (dialog->prompt_label),
+                       _("To rate and review software, you need an Ubuntu Single Sign-On account."));
+
+       return GTK_WIDGET (dialog);
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/plugins/gs-ubuntuone-dialog.h b/src/plugins/gs-ubuntuone-dialog.h
new file mode 100644
index 0000000..d98404e
--- /dev/null
+++ b/src/plugins/gs-ubuntuone-dialog.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef GS_UBUNTUONE_DIALOG_H
+#define GS_UBUNTUONE_DIALOG_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_UBUNTUONE_DIALOG gs_ubuntuone_dialog_get_type ()
+
+G_DECLARE_FINAL_TYPE (GsUbuntuoneDialog, gs_ubuntuone_dialog, GS, UBUNTUONE_DIALOG, GtkDialog)
+
+GtkWidget      *gs_ubuntuone_dialog_new                        (gboolean           get_macaroon);
+gboolean        gs_ubuntuone_dialog_get_do_remember            (GsUbuntuoneDialog *dialog);
+GVariant       *gs_ubuntuone_dialog_get_macaroon               (GsUbuntuoneDialog *dialog);
+const gchar    *gs_ubuntuone_dialog_get_consumer_key           (GsUbuntuoneDialog *dialog);
+const gchar    *gs_ubuntuone_dialog_get_consumer_secret        (GsUbuntuoneDialog *dialog);
+const gchar    *gs_ubuntuone_dialog_get_token_key              (GsUbuntuoneDialog *dialog);
+const gchar    *gs_ubuntuone_dialog_get_token_secret           (GsUbuntuoneDialog *dialog);
+
+G_END_DECLS
+
+#endif /* GS_UBUNTUONE_DIALOG_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/plugins/gs-ubuntuone-dialog.ui b/src/plugins/gs-ubuntuone-dialog.ui
new file mode 100644
index 0000000..e61c09e
--- /dev/null
+++ b/src/plugins/gs-ubuntuone-dialog.ui
@@ -0,0 +1,386 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.19.0 -->
+<interface>
+  <requires lib="gtk+" version="3.16"/>
+  <template class="GsUbuntuoneDialog" parent="GtkDialog">
+    <action-widgets>
+      <action-widget response="cancel">cancel_button</action-widget>
+    </action-widgets>
+    <child internal-child="headerbar">
+      <object class="GtkHeaderBar">
+        <property name="show_close_button">False</property>
+        <child>
+          <object class="GtkButton" id="cancel_button">
+            <property name="label" translatable="yes">_Cancel</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+          </object>
+          <packing>
+            <property name="pack-type">start</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="next_button">
+            <property name="label" translatable="yes">_Continue</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="can_default">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+            <style>
+              <class name="suggested-action"/>
+            </style>
+          </object>
+          <packing>
+            <property name="pack-type">end</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child internal-child="vbox">
+      <object class="GtkBox" id="content_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">20</property>
+        <property name="margin_right">20</property>
+        <property name="margin_top">20</property>
+        <property name="margin_bottom">20</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">40</property>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="spacing">20</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="yalign">0</property>
+                <property name="resource">/org/gnome/Software/plugins/ubuntu-one.png</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkStack" id="page_stack">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <child>
+                  <object class="GtkGrid" id="page-0">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkLabel" id="prompt_label">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="margin_bottom">20</property>
+                        <property name="label" translatable="yes">To rate and review software, you need an 
Ubuntu Single Sign-On account.</property>
+                        <property name="wrap">True</property>
+                        <property name="xalign">0</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">0</property>
+                        <property name="width">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkAccelLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="halign">end</property>
+                        <property name="margin_right">10</property>
+                        <property name="margin_bottom">20</property>
+                        <property name="label" translatable="yes">_Email address:</property>
+                        <property name="use_underline">True</property>
+                        <property name="xalign">1</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkEntry" id="email_entry">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="margin_bottom">20</property>
+                        <property name="hexpand">True</property>
+                        <property name="input_purpose">email</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkRadioButton" id="login_radio">
+                        <property name="label" translatable="yes">I have an Ubuntu Single Sign-On 
account</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="margin_bottom">5</property>
+                        <property name="xalign">0</property>
+                        <property name="active">True</property>
+                        <property name="draw_indicator">True</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">2</property>
+                        <property name="width">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkAccelLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="halign">end</property>
+                        <property name="margin_left">25</property>
+                        <property name="margin_right">10</property>
+                        <property name="margin_bottom">5</property>
+                        <property name="label" translatable="yes">_Password:</property>
+                        <property name="use_underline">True</property>
+                        <property name="xalign">1</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">3</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkEntry" id="password_entry">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="margin_bottom">5</property>
+                        <property name="hexpand">True</property>
+                        <property name="visibility">False</property>
+                        <property name="invisible_char">•</property>
+                        <property name="input_purpose">password</property>
+                        <property name="activates_default">True</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">3</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton" id="remember_check">
+                        <property name="label" translatable="yes">Sign in automatically next time</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="margin_bottom">20</property>
+                        <property name="hexpand">True</property>
+                        <property name="xalign">0</property>
+                        <property name="draw_indicator">True</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">4</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkRadioButton" id="register_radio">
+                        <property name="label" translatable="yes">I want to register for an account 
now</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="margin_bottom">20</property>
+                        <property name="xalign">0</property>
+                        <property name="active">True</property>
+                        <property name="draw_indicator">True</property>
+                        <property name="group">login_radio</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">5</property>
+                        <property name="width">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkRadioButton" id="reset_radio">
+                        <property name="label" translatable="yes">I've forgotten my password</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">False</property>
+                        <property name="xalign">0</property>
+                        <property name="active">True</property>
+                        <property name="draw_indicator">True</property>
+                        <property name="group">login_radio</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">6</property>
+                        <property name="width">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <placeholder/>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="name">page-0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkGrid" id="page-1">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="margin_bottom">20</property>
+                        <property name="label" translatable="yes">Enter your one-time password for 
two-factor authentication.</property>
+                        <property name="wrap">True</property>
+                        <property name="xalign">0</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">0</property>
+                        <property name="width">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkAccelLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="margin_right">10</property>
+                        <property name="label" translatable="yes">One-time password:</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkEntry" id="passcode_entry">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="hexpand">True</property>
+                        <property name="input_purpose">pin</property>
+                        <property name="activates_default">True</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="name">page-1</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkGrid" id="page-2">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="yes">You are now signed into Ubuntu 
One.</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="name">page-2</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="spacing">10</property>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <child>
+                  <object class="GtkStack" id="status_stack">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="margin_right">5</property>
+                    <child>
+                      <object class="GtkImage" id="status_image">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                      </object>
+                      <packing>
+                        <property name="name">status-image</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSpinner" id="status_spinner">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="active">True</property>
+                      </object>
+                      <packing>
+                        <property name="name">status-spinner</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="status_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="xalign">0</property>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/gs-ubuntuone.c b/src/plugins/gs-ubuntuone.c
new file mode 100644
index 0000000..3c7b582
--- /dev/null
+++ b/src/plugins/gs-ubuntuone.c
@@ -0,0 +1,410 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <config.h>
+
+#include <libsecret/secret.h>
+
+#include <gs-plugin.h>
+
+#include "gs-ubuntuone.h"
+#include "gs-ubuntuone-dialog.h"
+
+#define SCHEMA_NAME     "com.ubuntu.UbuntuOne.GnomeSoftware"
+#define MACAROON        "macaroon"
+#define CONSUMER_KEY    "consumer-key"
+#define CONSUMER_SECRET "consumer-secret"
+#define TOKEN_KEY       "token-key"
+#define TOKEN_SECRET    "token-secret"
+
+static SecretSchema schema = {
+       SCHEMA_NAME,
+       SECRET_SCHEMA_NONE,
+       { { "key", SECRET_SCHEMA_ATTRIBUTE_STRING } }
+};
+
+typedef struct
+{
+       GError **error;
+
+       GCond cond;
+       GMutex mutex;
+
+       gboolean get_macaroon;
+
+       gboolean done;
+       gboolean success;
+       gboolean remember;
+
+       GVariant *macaroon;
+       gchar *consumer_key;
+       gchar *consumer_secret;
+       gchar *token_key;
+       gchar *token_secret;
+} LoginContext;
+
+static gboolean
+show_login_dialog (gpointer user_data)
+{
+       LoginContext *context = user_data;
+       GtkWidget *dialog;
+
+       dialog = gs_ubuntuone_dialog_new (context->get_macaroon);
+
+       switch (gtk_dialog_run (GTK_DIALOG (dialog))) {
+       case GTK_RESPONSE_DELETE_EVENT:
+       case GTK_RESPONSE_CANCEL:
+               if (context->get_macaroon) {
+                       g_set_error (context->error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "Unable to obtain snapd macaroon");
+               } else {
+                       g_set_error (context->error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "Unable to sign into Ubuntu One");
+               }
+
+               context->success = FALSE;
+               break;
+
+       case GTK_RESPONSE_OK:
+               context->remember = gs_ubuntuone_dialog_get_do_remember (GS_UBUNTUONE_DIALOG (dialog));
+               context->macaroon = gs_ubuntuone_dialog_get_macaroon (GS_UBUNTUONE_DIALOG (dialog));
+               context->consumer_key = g_strdup (gs_ubuntuone_dialog_get_consumer_key (GS_UBUNTUONE_DIALOG 
(dialog)));
+               context->consumer_secret = g_strdup (gs_ubuntuone_dialog_get_consumer_secret 
(GS_UBUNTUONE_DIALOG (dialog)));
+               context->token_key = g_strdup (gs_ubuntuone_dialog_get_token_key (GS_UBUNTUONE_DIALOG 
(dialog)));
+               context->token_secret = g_strdup (gs_ubuntuone_dialog_get_token_secret (GS_UBUNTUONE_DIALOG 
(dialog)));
+               context->success = TRUE;
+
+               if (context->macaroon != NULL)
+                       g_variant_ref (context->macaroon);
+
+               break;
+       }
+
+       gtk_widget_destroy (dialog);
+
+       g_mutex_lock (&context->mutex);
+       context->done = TRUE;
+       g_cond_signal (&context->cond);
+       g_mutex_unlock (&context->mutex);
+
+       return G_SOURCE_REMOVE;
+}
+
+GVariant *
+gs_ubuntuone_get_macaroon (gboolean   use_cache,
+                          gboolean   show_dialog,
+                          GError   **error)
+{
+       LoginContext login_context = { 0 };
+       g_autofree gchar *password = NULL;
+       g_autofree gchar *printed = NULL;
+       GVariant *macaroon = NULL;
+       GError *error_local = NULL;
+
+       if (use_cache) {
+               password = secret_password_lookup_sync (&schema,
+                                                       NULL,
+                                                       &error_local,
+                                                       "key", MACAROON,
+                                                       NULL);
+
+               if (password) {
+                       macaroon = g_variant_parse (G_VARIANT_TYPE ("(sas)"),
+                                                   password,
+                                                   NULL,
+                                                   NULL,
+                                                   &error_local);
+
+                       if (macaroon)
+                               return macaroon;
+
+                       g_warning ("could not parse macaroon: %s", error_local->message);
+                       g_clear_error (&error_local);
+               } else if (error_local != NULL) {
+                       g_warning ("could not lookup cached macaroon: %s", error_local->message);
+                       g_clear_error (&error_local);
+               }
+       }
+
+       if (show_dialog) {
+               /* Pop up a login dialog */
+               login_context.error = error;
+               login_context.get_macaroon = TRUE;
+               g_cond_init (&login_context.cond);
+               g_mutex_init (&login_context.mutex);
+               g_mutex_lock (&login_context.mutex);
+
+               gdk_threads_add_idle (show_login_dialog, &login_context);
+
+               while (!login_context.done)
+                       g_cond_wait (&login_context.cond, &login_context.mutex);
+
+               g_mutex_unlock (&login_context.mutex);
+               g_mutex_clear (&login_context.mutex);
+               g_cond_clear (&login_context.cond);
+
+               if (login_context.macaroon != NULL && login_context.remember) {
+                       printed = g_variant_print (login_context.macaroon, FALSE);
+
+                       if (!secret_password_store_sync (&schema,
+                                                        NULL,
+                                                        SCHEMA_NAME,
+                                                        printed,
+                                                        NULL,
+                                                        &error_local,
+                                                        "key", MACAROON,
+                                                        NULL)) {
+                               g_warning ("could not store macaroon: %s", error_local->message);
+                               g_clear_error (&error_local);
+                       }
+               }
+
+               return login_context.macaroon;
+       }
+
+       return NULL;
+}
+
+void
+gs_ubuntuone_clear_macaroon (void)
+{
+       secret_password_clear_sync (&schema, NULL, NULL, "key", MACAROON, NULL);
+}
+
+typedef struct
+{
+       GCancellable *cancellable;
+       GCond cond;
+       GMutex mutex;
+
+       gint waiting;
+
+       gchar *consumer_key;
+       gchar *consumer_secret;
+       gchar *token_key;
+       gchar *token_secret;
+} SecretContext;
+
+static void
+lookup_consumer_key (GObject      *source_object,
+                    GAsyncResult *result,
+                    gpointer      user_data)
+{
+       SecretContext *context = user_data;
+
+       context->consumer_key = secret_password_lookup_finish (result, NULL);
+
+       g_mutex_lock (&context->mutex);
+
+       context->waiting--;
+
+       g_cond_signal (&context->cond);
+       g_mutex_unlock (&context->mutex);
+}
+
+static void
+lookup_consumer_secret (GObject      *source_object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+       SecretContext *context = user_data;
+
+       context->consumer_secret = secret_password_lookup_finish (result, NULL);
+
+       g_mutex_lock (&context->mutex);
+
+       context->waiting--;
+
+       g_cond_signal (&context->cond);
+       g_mutex_unlock (&context->mutex);
+}
+
+static void
+lookup_token_key (GObject      *source_object,
+                 GAsyncResult *result,
+                 gpointer      user_data)
+{
+       SecretContext *context = user_data;
+
+       context->token_key = secret_password_lookup_finish (result, NULL);
+
+       g_mutex_lock (&context->mutex);
+
+       context->waiting--;
+
+       g_cond_signal (&context->cond);
+       g_mutex_unlock (&context->mutex);
+}
+
+static void
+lookup_token_secret (GObject      *source_object,
+                    GAsyncResult *result,
+                    gpointer      user_data)
+{
+       SecretContext *context = user_data;
+
+       context->token_secret = secret_password_lookup_finish (result, NULL);
+
+       g_mutex_lock (&context->mutex);
+
+       context->waiting--;
+
+       g_cond_signal (&context->cond);
+       g_mutex_unlock (&context->mutex);
+}
+
+gboolean
+gs_ubuntuone_get_credentials (gchar **consumer_key, gchar **consumer_secret, gchar **token_key, gchar 
**token_secret)
+{
+       SecretContext secret_context = { 0 };
+
+       /* Use credentials from libsecret if available */
+       secret_context.waiting = 4;
+       secret_context.cancellable = g_cancellable_new ();
+       g_cond_init (&secret_context.cond);
+       g_mutex_init (&secret_context.mutex);
+       g_mutex_lock (&secret_context.mutex);
+
+       secret_password_lookup (&schema,
+                               secret_context.cancellable,
+                               lookup_consumer_key,
+                               &secret_context,
+                               "key", CONSUMER_KEY,
+                               NULL);
+       secret_password_lookup (&schema,
+                               secret_context.cancellable,
+                               lookup_consumer_secret,
+                               &secret_context,
+                               "key", CONSUMER_SECRET,
+                               NULL);
+       secret_password_lookup (&schema,
+                               secret_context.cancellable,
+                               lookup_token_key,
+                               &secret_context,
+                               "key", TOKEN_KEY,
+                               NULL);
+       secret_password_lookup (&schema,
+                               secret_context.cancellable,
+                               lookup_token_secret,
+                               &secret_context,
+                               "key", TOKEN_SECRET,
+                               NULL);
+
+       while (secret_context.waiting > 0)
+               g_cond_wait (&secret_context.cond, &secret_context.mutex);
+
+       g_mutex_unlock (&secret_context.mutex);
+       g_mutex_clear (&secret_context.mutex);
+       g_cond_clear (&secret_context.cond);
+       g_cancellable_cancel (secret_context.cancellable);
+       g_clear_object (&secret_context.cancellable);
+
+       if (secret_context.consumer_key != NULL &&
+           secret_context.consumer_secret != NULL &&
+           secret_context.token_key != NULL &&
+           secret_context.token_secret != NULL) {
+               *consumer_key = secret_context.consumer_key;
+               *consumer_secret = secret_context.consumer_secret;
+               *token_key = secret_context.token_key;
+               *token_secret = secret_context.token_secret;
+               return TRUE;
+       }
+
+       g_free (secret_context.token_secret);
+       g_free (secret_context.token_key);
+       g_free (secret_context.consumer_secret);
+       g_free (secret_context.consumer_key);
+       return FALSE;
+}
+
+gboolean
+gs_ubuntuone_sign_in (gchar **consumer_key, gchar **consumer_secret, gchar **token_key, gchar 
**token_secret, GError **error)
+{
+       LoginContext login_context = { 0 };
+
+       /* Pop up a login dialog */
+       login_context.error = error;
+       login_context.get_macaroon = FALSE;
+       g_cond_init (&login_context.cond);
+       g_mutex_init (&login_context.mutex);
+       g_mutex_lock (&login_context.mutex);
+
+       gdk_threads_add_idle (show_login_dialog, &login_context);
+
+       while (!login_context.done)
+               g_cond_wait (&login_context.cond, &login_context.mutex);
+
+       g_mutex_unlock (&login_context.mutex);
+       g_mutex_clear (&login_context.mutex);
+       g_cond_clear (&login_context.cond);
+
+       if (login_context.remember) {
+               secret_password_store (&schema,
+                                      NULL,
+                                      SCHEMA_NAME,
+                                      login_context.consumer_key,
+                                      NULL,
+                                      NULL,
+                                      NULL,
+                                      "key", CONSUMER_KEY,
+                                      NULL);
+
+               secret_password_store (&schema,
+                                      NULL,
+                                      SCHEMA_NAME,
+                                      login_context.consumer_secret,
+                                      NULL,
+                                      NULL,
+                                      NULL,
+                                      "key", CONSUMER_SECRET,
+                                      NULL);
+
+               secret_password_store (&schema,
+                                      NULL,
+                                      SCHEMA_NAME,
+                                      login_context.token_key,
+                                      NULL,
+                                      NULL,
+                                      NULL,
+                                      "key", TOKEN_KEY,
+                                      NULL);
+
+               secret_password_store (&schema,
+                                      NULL,
+                                      SCHEMA_NAME,
+                                      login_context.token_secret,
+                                      NULL,
+                                      NULL,
+                                      NULL,
+                                      "key", TOKEN_SECRET,
+                                      NULL);
+       }
+
+       *consumer_key = login_context.consumer_key;
+       *consumer_secret = login_context.consumer_secret;
+       *token_key = login_context.token_key;
+       *token_secret = login_context.token_secret;
+       return login_context.success;
+}
diff --git a/src/plugins/gs-ubuntuone.h b/src/plugins/gs-ubuntuone.h
new file mode 100644
index 0000000..4e9fde8
--- /dev/null
+++ b/src/plugins/gs-ubuntuone.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_UBUNTUONE_H
+#define __GS_UBUNTUONE_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+GVariant       *gs_ubuntuone_get_macaroon      (gboolean         use_cache,
+                                                gboolean         show_dialog,
+                                                GError         **error);
+
+void            gs_ubuntuone_clear_macaroon    (void);
+
+gboolean        gs_ubuntuone_get_credentials   (gchar  **consumer_key,
+                                                gchar  **consumer_secret,
+                                                gchar  **token_key,
+                                                gchar  **token_secret);
+
+gboolean        gs_ubuntuone_sign_in   (gchar  **consumer_key,
+                                        gchar  **consumer_secret,
+                                        gchar  **token_key,
+                                        gchar  **token_secret,
+                                        GError **error);
+
+G_END_DECLS
+
+#endif /* __GS_UBUNTUONE_H */
+
diff --git a/src/plugins/ubuntu-one.png b/src/plugins/ubuntu-one.png
new file mode 100644
index 0000000..a58248a
Binary files /dev/null and b/src/plugins/ubuntu-one.png differ


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]