diffstat of debian/ for gnome-software_3.27.90-1 gnome-software_3.28.1-0ubuntu4 README.source | 9 changelog | 157 control | 33 control.in | 29 extra/60gnome-software | 11 gbp.conf | 5 gnome-software.gsettings-override.in | 6 gnome-software.install | 1 gnome-software.maintscript | 1 icons/hicolor/128x128/apps/ubuntusoftware.svg | 2293 ++++++++++ icons/hicolor/16x16/apps/ubuntusoftware.svg | 564 ++ icons/hicolor/24x24/apps/ubuntusoftware.svg | 678 ++ icons/hicolor/48x48/apps/ubuntusoftware.svg | 666 ++ icons/hicolor/scalable/apps/ubuntusoftware.svg | 2293 ++++++++++ patches/0001-Construct-the-Software-Sources-menu-item-dynamically.patch | 73 patches/0001-Fix-gs_app_set_state_recover-not-working-during-purc.patch | 25 patches/0001-snap-Only-load-icons-once.patch | 25 patches/0001-snap-Scale-embedded-snap-icons-to-64x64.patch | 50 patches/0002-Download-changelog-information-on-demand-this-stops-.patch | 113 patches/0003-Sort-snaps-before-other-apps.patch | 29 patches/0004-Hide-Kudo-details-since-we-don-t-have-good-data.patch | 43 patches/0005-details-Show-an-in-app-notification-when-passed-an-i.patch | 186 patches/0006-packagekit-Disable-updates.patch | 28 patches/0007-snap-Only-feature-snaps.patch | 65 patches/0008-Don-t-randomize-editors-picks.patch | 29 patches/0009-Display-a-warning-for-non-sandboxed-snaps.patch | 121 patches/0010-Sort-category-snaps-before-other-packages.patch | 41 patches/0011-Support-snap-channels.patch | 1710 +++++++ patches/0012-Don-t-use-colour-to-differentiate-between-free-and-p.patch | 38 patches/0013-overview-page-Rotate-featured-apps.patch | 357 + patches/0014-Add-a-basic-permissions-system.patch | 1983 ++++++++ patches/0015-Don-t-reject-unexpected-state-changes-external-event.patch | 26 patches/0016-build-Translate-Ubuntu-s-.desktop-file.patch | 64 patches/0017-snap-Use-default-icon-if-none-provided.patch | 93 patches/0018-snap-Make-snaps-purchasable.patch | 125 patches/0019-Disable-paid-snap-support-unless-env-variable-GNOME_.patch | 33 patches/series | 25 ubuntu-software.install | 2 ubuntu-software.links | 2 39 files changed, 11998 insertions(+), 34 deletions(-) diff -Nru gnome-software-3.27.90/debian/README.source gnome-software-3.28.1/debian/README.source --- gnome-software-3.27.90/debian/README.source 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/README.source 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,9 @@ +The Ubuntu packaging is maintained at +https://code.launchpad.net/~ubuntu-desktop/gnome-software/ubuntu + +Patches are maintained at https://gitlab.gnome.org/ubuntu/gnome-software +in ubuntu-* branches. These branches are regularly rebased. + +To inspect these changes individually, please issue + + $ git clone -b ubuntu-3-26 https://gitlab.gnome.org/ubuntu/gnome-software.git diff -Nru gnome-software-3.27.90/debian/changelog gnome-software-3.28.1/debian/changelog --- gnome-software-3.27.90/debian/changelog 2018-02-18 21:27:18.000000000 +0000 +++ gnome-software-3.28.1/debian/changelog 2018-04-19 03:22:29.000000000 +0000 @@ -1,3 +1,160 @@ +gnome-software (3.28.1-0ubuntu4) bionic; urgency=medium + + * debian/patches/0001-Fix-gs_app_set_state_recover-not-working-during-purc.patch: + - Fix purchase button disappearing when setting up credit card + * debian/patches/0019-Disable-paid-snap-support-unless-env-variable-GNOME_.patch: + - Disable paid support for now (enable by setting the + GNOME_SOFTWARE_SHOW_PAID environment variable) + + -- Robert Ancell Thu, 19 Apr 2018 15:22:29 +1200 + +gnome-software (3.28.1-0ubuntu3) bionic; urgency=medium + + * Merge change from Debian: + - Drop icon-download APT config snippet, depend on shared apt-config-icons + instead (LP: #1764589) + + -- Jeremy Bicha Mon, 16 Apr 2018 21:25:08 -0400 + +gnome-software (3.28.1-0ubuntu2) bionic; urgency=medium + + * debian/patches/0017-snap-Use-default-icon-if-none-provided.patch: + - Fix snaps without icons not showing (LP: #1763736) + * debian/patches/0018-snap-Make-snaps-purchasable.patch: + - Support purchasable snaps (LP: #1547135) + * debian/patches/0001-snap-Scale-embedded-snap-icons-to-64x64.patch: + * debian/patches/0001-snap-Only-load-icons-once.patch: + - Fix snap icon sizes and stop duplicate loading + + -- Robert Ancell Mon, 16 Apr 2018 15:32:27 +1200 + +gnome-software (3.28.1-0ubuntu1) bionic; urgency=medium + + * New upstream stable release + + -- Robert Ancell Wed, 11 Apr 2018 11:32:17 +1200 + +gnome-software (3.28.0-0ubuntu7) bionic; urgency=medium + + * debian/patches/0014-Add-a-basic-permissions-system.patch: + - Fix dialog title not being translatable (LP: #1758207) + + -- Robert Ancell Fri, 23 Mar 2018 13:59:05 +1300 + +gnome-software (3.28.0-0ubuntu6) bionic; urgency=medium + + * debian/patches/0016-Translate-ubuntu-desktop-file.patch: + * debian/rules: + * debian/ubuntu-software.install: + - Translate Ubuntu's .desktop file (LP: #1756982). + + -- Gunnar Hjalmarsson Wed, 21 Mar 2018 15:10:00 +0100 + +gnome-software (3.28.0-0ubuntu5) bionic; urgency=medium + + [ Jeremy Bicha ] + * debian/ubuntu-software.links: + - Update for renamed Ubuntu on Wayland session + + [ Robert Ancell ] + * debian/patches/0011-Support-snap-channels.patch: + - Don't do round-trip to server for featured snaps - only need + to do this on details page to show channel information. (LP: #1756184) + + -- Robert Ancell Wed, 21 Mar 2018 16:11:28 +1300 + +gnome-software (3.28.0-0ubuntu4) bionic; urgency=medium + + * debian/patches/0001-Stop-searching-multiple-times-on-search-page.patch: + - Stop duplicate search queries (LP: #1756184) + + -- Robert Ancell Fri, 16 Mar 2018 12:21:56 +1300 + +gnome-software (3.28.0-0ubuntu3) bionic; urgency=medium + + * debian/patches/0001-snap-Only-refine-screenshots-when-requested.patch: + * debian/patches/0011-Support-snap-channels.patch: + - Avoid crash populating snap screenshots and channels (LP: #1755198) + + -- Robert Ancell Wed, 14 Mar 2018 16:43:28 +1300 + +gnome-software (3.28.0-0ubuntu2) bionic; urgency=medium + + * debian/patches/0014-Add-a-basic-permissions-system.patch: + - Fix spelling mistakes (LP: #1755395) + * debian/patches/0005-details-Show-an-in-app-notification-when-passed-an-i.patch: + - Fix crash following snap URLs (LP: #1752645) + + -- Robert Ancell Wed, 14 Mar 2018 15:27:10 +1300 + +gnome-software (3.28.0-0ubuntu1) bionic; urgency=medium + + * New upstream release + * debian/patches/0001-snap-Fix-invalid-metadata-after-cancelled-refine.patch: + - Applied upstream + + -- Robert Ancell Tue, 13 Mar 2018 17:00:42 +1300 + +gnome-software (3.27.92-0ubuntu3) bionic; urgency=medium + + * debian/patches/0014-Add-a-basic-permissions-system.patch: + - Fix string not marked for translation (LP: #1754903) + + -- Robert Ancell Tue, 13 Mar 2018 12:52:50 +1300 + +gnome-software (3.27.92-0ubuntu2) bionic; urgency=medium + + * debian/patches/0001-snap-Fix-invalid-metadata-after-cancelled-refine.patch: + * debian/patches/0015-Don-t-reject-unexpected-state-changes-external-event.patch: + - Fix snaps not being shown correctly after install from command line + (LP: #1754655) + + -- Robert Ancell Fri, 09 Mar 2018 13:40:18 +0100 + +gnome-software (3.27.92-0ubuntu1) bionic; urgency=medium + + * New upstream release + * debian/patches/0001-Don-t-use-app-ID-as-a-tie-break-when-ordering-search.patch + * debian/patches/0001-Make-app-name-sorting-case-insensitive.patch + * debian/patches/0001-Revert-snap-Show-revision-number-in-version-field.patch + * debian/patches/0001-snap-Refactor-how-metadata-is-merged-between-install.patch + - Applied upstream + * debian/patches/0009-Display-a-warning-for-non-sandboxed-snaps.patch: + - Update warning text + * debian/patches/0012-Don-t-use-colour-to-differentiate-between-free-and-p.patch + - Don't use red to indicate propietary software (LP: #1750533) + * debian/patches/0013-overview-page-Rotate-featured-apps.patch: + - Support showing multiple banners (LP: #1750548) + * debian/patches/0014-Add-a-basic-permissions-system.patch: + - Add basic system for connecting/disconnecting snap interfaces + (LP: #1597314) + + -- Robert Ancell Tue, 06 Mar 2018 16:46:42 +0100 + +gnome-software (3.27.90-1ubuntu2) bionic; urgency=medium + + * debian/gnome-software.gsettings-override.in: + - Update for renamed gsetting keys + + -- Tim Lunn Sat, 03 Mar 2018 16:48:11 +1100 + +gnome-software (3.27.90-1ubuntu1) bionic; urgency=medium + + * Merge with Debian, remaining changes: + + debian/patches/*.patch: Various Ubuntu changes from ubuntu-master + branch - see patch headers for more information. + + Add an "ubuntu-software" package with some branding for Ubuntu. + + debian/gnome-software.gsettings-override.in: + - Mark universe packages as Free. + + debian/control.in: + - Recommend Snap plugin + - GNOME Software provides the PackageKit session interface. + Sessioninstaller wants to do that too - Conflict with it to get it + off user systems. + * Upstream changes fix (LP: #1665126) and (LP: #1750030) + + -- Robert Ancell Tue, 20 Feb 2018 11:36:59 +1300 + gnome-software (3.27.90-1) experimental; urgency=medium * New upstream development release diff -Nru gnome-software-3.27.90/debian/control gnome-software-3.28.1/debian/control --- gnome-software-3.27.90/debian/control 2018-02-18 21:27:18.000000000 +0000 +++ gnome-software-3.28.1/debian/control 2018-04-19 03:22:29.000000000 +0000 @@ -1,12 +1,13 @@ # This file is autogenerated. DO NOT EDIT! -# +# # Modifications should be made to debian/control.in instead. # This file is regenerated automatically in the clean target. Source: gnome-software Section: gnome Priority: optional -Maintainer: Debian GNOME Maintainers -Uploaders: Jeremy Bicha , Michael Biebl +Maintainer: Ubuntu Developers +XSBC-Original-Maintainer: Debian GNOME Maintainers +Uploaders: Jeremy Bicha , Michael Biebl , Tim Lunn Build-Depends: appstream, appstream-util, debhelper (>= 11), @@ -38,13 +39,13 @@ valgrind [amd64 arm64 armhf i386 mips mips64 mips64el mipsel powerpc ppc64 ppc64el s390x], xsltproc Standards-Version: 4.1.2 -Vcs-Browser: https://salsa.debian.org/gnome-team/gnome-software -Vcs-Git: https://salsa.debian.org/gnome-team/gnome-software.git Homepage: https://wiki.gnome.org/Apps/Software +Vcs-Bzr: https://code.launchpad.net/~ubuntu-desktop/gnome-software/ubuntu Package: gnome-software Architecture: any Depends: appstream, + apt-config-icons, gnome-software-common (= ${source:Version}), gsettings-desktop-schemas (>= 3.18), packagekit (>= 1.1.4), @@ -53,10 +54,12 @@ ${shlibs:Depends} Replaces: gnome-packagekit-session (<< 3.16.0-2~) Breaks: gnome-packagekit-session (<< 3.16.0-2~) -Suggests: fwupd [amd64 arm64 armhf i386], +Conflicts: sessioninstaller +Recommends: gnome-software-plugin-snap [linux-any], +Suggests: apt-config-icons-hidpi, + fwupd [amd64 arm64 armhf i386], gnome-software-plugin-flatpak [linux-any], - gnome-software-plugin-limba [linux-any], - gnome-software-plugin-snap [linux-any] + gnome-software-plugin-limba [linux-any] Description: Software Center for GNOME Software lets you install and update applications and system extensions. . @@ -175,3 +178,17 @@ . This package contains documentation for use when developing plugins for Software. + +Package: ubuntu-software +Architecture: all +Depends: gnome-software (>= ${source:Version}), + ${misc:Depends}, + ${shlibs:Depends} +Description: Utility for browsing, installing, and removing software + Ubuntu Software lets you browse and install the applications available for + Ubuntu. You can view available software by category, or search by name + or description. You can also examine the software already installed, and + remove items you no longer need. + . + To install or remove software using Ubuntu Software, you need administrator + access on the computer. diff -Nru gnome-software-3.27.90/debian/control.in gnome-software-3.28.1/debian/control.in --- gnome-software-3.27.90/debian/control.in 2018-02-18 21:27:18.000000000 +0000 +++ gnome-software-3.28.1/debian/control.in 2018-04-19 03:22:29.000000000 +0000 @@ -1,7 +1,8 @@ Source: gnome-software Section: gnome Priority: optional -Maintainer: Debian GNOME Maintainers +Maintainer: Ubuntu Developers +XSBC-Original-Maintainer: Debian GNOME Maintainers Uploaders: @GNOME_TEAM@ Build-Depends: appstream, appstream-util, @@ -34,13 +35,13 @@ valgrind [amd64 arm64 armhf i386 mips mips64 mips64el mipsel powerpc ppc64 ppc64el s390x], xsltproc Standards-Version: 4.1.2 -Vcs-Browser: https://salsa.debian.org/gnome-team/gnome-software -Vcs-Git: https://salsa.debian.org/gnome-team/gnome-software.git Homepage: https://wiki.gnome.org/Apps/Software +Vcs-Bzr: https://code.launchpad.net/~ubuntu-desktop/gnome-software/ubuntu Package: gnome-software Architecture: any Depends: appstream, + apt-config-icons, gnome-software-common (= ${source:Version}), gsettings-desktop-schemas (>= 3.18), packagekit (>= 1.1.4), @@ -49,10 +50,12 @@ ${shlibs:Depends} Replaces: gnome-packagekit-session (<< 3.16.0-2~) Breaks: gnome-packagekit-session (<< 3.16.0-2~) -Suggests: fwupd [amd64 arm64 armhf i386], +Conflicts: sessioninstaller +Recommends: gnome-software-plugin-snap [linux-any], +Suggests: apt-config-icons-hidpi, + fwupd [amd64 arm64 armhf i386], gnome-software-plugin-flatpak [linux-any], - gnome-software-plugin-limba [linux-any], - gnome-software-plugin-snap [linux-any] + gnome-software-plugin-limba [linux-any] Description: Software Center for GNOME Software lets you install and update applications and system extensions. . @@ -171,3 +174,17 @@ . This package contains documentation for use when developing plugins for Software. + +Package: ubuntu-software +Architecture: all +Depends: gnome-software (>= ${source:Version}), + ${misc:Depends}, + ${shlibs:Depends} +Description: Utility for browsing, installing, and removing software + Ubuntu Software lets you browse and install the applications available for + Ubuntu. You can view available software by category, or search by name + or description. You can also examine the software already installed, and + remove items you no longer need. + . + To install or remove software using Ubuntu Software, you need administrator + access on the computer. diff -Nru gnome-software-3.27.90/debian/extra/60gnome-software gnome-software-3.28.1/debian/extra/60gnome-software --- gnome-software-3.27.90/debian/extra/60gnome-software 2018-02-18 21:27:18.000000000 +0000 +++ gnome-software-3.28.1/debian/extra/60gnome-software 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -## This file is provided by GNOME Software to enable the download of -## small-size 64x64px icons for use in the software catalog. -## -## The icons are required for GNOME Software to function correctly. -## Applications without an icon will not be displayed at all. - -Acquire::IndexTargets { - deb::DEP-11-icons { - DefaultEnabled "true"; - }; -}; diff -Nru gnome-software-3.27.90/debian/gbp.conf gnome-software-3.28.1/debian/gbp.conf --- gnome-software-3.27.90/debian/gbp.conf 2018-02-18 21:27:18.000000000 +0000 +++ gnome-software-3.28.1/debian/gbp.conf 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -[DEFAULT] -pristine-tar = True -debian-branch = debian/master -upstream-branch = upstream/latest -upstream-vcs-tag = %(version)s diff -Nru gnome-software-3.27.90/debian/gnome-software.gsettings-override.in gnome-software-3.28.1/debian/gnome-software.gsettings-override.in --- gnome-software-3.27.90/debian/gnome-software.gsettings-override.in 2018-02-18 21:27:18.000000000 +0000 +++ gnome-software-3.28.1/debian/gnome-software.gsettings-override.in 2018-04-19 03:22:29.000000000 +0000 @@ -1,4 +1,4 @@ [org.gnome.software] -official-sources=['@DISTRO@-*'] -free-sources=['@DISTRO@-*-main'] -free-sources-url='@URL@' +official-repos=['@DISTRO@-*'] +free-repos=['@DISTRO@-*-main', '@DISTRO@-*-universe'] +free-repos-url='@URL@' diff -Nru gnome-software-3.27.90/debian/gnome-software.install gnome-software-3.28.1/debian/gnome-software.install --- gnome-software-3.27.90/debian/gnome-software.install 2018-02-18 21:27:18.000000000 +0000 +++ gnome-software-3.28.1/debian/gnome-software.install 2018-04-19 03:22:29.000000000 +0000 @@ -1,4 +1,3 @@ -debian/extra/60gnome-software /etc/apt/apt.conf.d/ etc/xdg/autostart/ usr/bin/gnome-software usr/bin/gnome-software-editor diff -Nru gnome-software-3.27.90/debian/gnome-software.maintscript gnome-software-3.28.1/debian/gnome-software.maintscript --- gnome-software-3.27.90/debian/gnome-software.maintscript 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/gnome-software.maintscript 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1 @@ +rm_conffile /etc/apt/apt.conf.d/60gnome-software 3.28.1-0ubuntu3~ diff -Nru gnome-software-3.27.90/debian/icons/hicolor/128x128/apps/ubuntusoftware.svg gnome-software-3.28.1/debian/icons/hicolor/128x128/apps/ubuntusoftware.svg --- gnome-software-3.27.90/debian/icons/hicolor/128x128/apps/ubuntusoftware.svg 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/icons/hicolor/128x128/apps/ubuntusoftware.svg 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,2293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru gnome-software-3.27.90/debian/icons/hicolor/16x16/apps/ubuntusoftware.svg gnome-software-3.28.1/debian/icons/hicolor/16x16/apps/ubuntusoftware.svg --- gnome-software-3.27.90/debian/icons/hicolor/16x16/apps/ubuntusoftware.svg 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/icons/hicolor/16x16/apps/ubuntusoftware.svg 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,564 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru gnome-software-3.27.90/debian/icons/hicolor/24x24/apps/ubuntusoftware.svg gnome-software-3.28.1/debian/icons/hicolor/24x24/apps/ubuntusoftware.svg --- gnome-software-3.27.90/debian/icons/hicolor/24x24/apps/ubuntusoftware.svg 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/icons/hicolor/24x24/apps/ubuntusoftware.svg 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,678 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru gnome-software-3.27.90/debian/icons/hicolor/48x48/apps/ubuntusoftware.svg gnome-software-3.28.1/debian/icons/hicolor/48x48/apps/ubuntusoftware.svg --- gnome-software-3.27.90/debian/icons/hicolor/48x48/apps/ubuntusoftware.svg 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/icons/hicolor/48x48/apps/ubuntusoftware.svg 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,666 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru gnome-software-3.27.90/debian/icons/hicolor/scalable/apps/ubuntusoftware.svg gnome-software-3.28.1/debian/icons/hicolor/scalable/apps/ubuntusoftware.svg --- gnome-software-3.27.90/debian/icons/hicolor/scalable/apps/ubuntusoftware.svg 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/icons/hicolor/scalable/apps/ubuntusoftware.svg 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,2293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -Nru gnome-software-3.27.90/debian/patches/0001-Construct-the-Software-Sources-menu-item-dynamically.patch gnome-software-3.28.1/debian/patches/0001-Construct-the-Software-Sources-menu-item-dynamically.patch --- gnome-software-3.27.90/debian/patches/0001-Construct-the-Software-Sources-menu-item-dynamically.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0001-Construct-the-Software-Sources-menu-item-dynamically.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,73 @@ +From 85fbad0fc4eda59c3266179beada553f6cb2d6f0 Mon Sep 17 00:00:00 2001 +From: Iain Lane +Date: Mon, 23 Jan 2017 16:22:09 +0000 +Subject: [PATCH 01/18] Construct the "Software Sources" menu item dynamically + +If we have software-properties-gtk, we'll launch that. In that case, +call the menu item "Software & Updates", since that more accurately +reflects what it does. +--- + src/gs-application.c | 23 ++++++++++++++++++++++- + src/gs-menus.ui | 8 +------- + 2 files changed, 23 insertions(+), 8 deletions(-) + +diff --git a/src/gs-application.c b/src/gs-application.c +index b3e30ed2..38bc212a 100644 +--- a/src/gs-application.c ++++ b/src/gs-application.c +@@ -920,11 +920,32 @@ gs_application_add_wrapper_actions (GApplication *application) + static void + gs_application_startup (GApplication *application) + { ++ g_autofree gchar *software_properties = NULL; ++ const gchar *label = NULL; ++ g_autoptr(GMenu) menu = NULL; ++ g_autoptr(GMenu) new = NULL; ++ + GSettings *settings; + GsApplication *app = GS_APPLICATION (application); + G_APPLICATION_CLASS (gs_application_parent_class)->startup (application); + +- gs_application_add_wrapper_actions (application); ++ /* This follows the behaviour in src/gs-shell.c; when we have s-p-gtk, ++ * we will launch it. It provides a UI to manage update behaviour too. ++ */ ++ software_properties = g_find_program_in_path ("software-properties-gtk"); ++ ++ if (!software_properties) ++ label = _("Software Sources"); ++ else ++ label = _("Software & Updates"); ++ ++ menu = gtk_application_get_menu_by_id (GTK_APPLICATION (application), ++ "app-menu"); ++ new = g_menu_new (); ++ g_menu_append (new, label, "app.sources"); ++ g_menu_prepend_section (menu, NULL, G_MENU_MODEL (new)); ++ ++ gs_application_add_wrapper_actions (application); + + g_action_map_add_action_entries (G_ACTION_MAP (application), + actions, G_N_ELEMENTS (actions), +diff --git a/src/gs-menus.ui b/src/gs-menus.ui +index 4ca62b7c..59e0b1e8 100644 +--- a/src/gs-menus.ui ++++ b/src/gs-menus.ui +@@ -2,13 +2,7 @@ + + + +-
+- +- _Software Repositories +- app.sources +- action-disabled +- +-
++ +
+ + _About +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0001-Fix-gs_app_set_state_recover-not-working-during-purc.patch gnome-software-3.28.1/debian/patches/0001-Fix-gs_app_set_state_recover-not-working-during-purc.patch --- gnome-software-3.27.90/debian/patches/0001-Fix-gs_app_set_state_recover-not-working-during-purc.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0001-Fix-gs_app_set_state_recover-not-working-during-purc.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,25 @@ +From 1448f5da9fbd6cc105e6a9190b4d2236a74190b7 Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Thu, 19 Apr 2018 15:05:12 +1200 +Subject: [PATCH] Fix gs_app_set_state_recover not working during purchase + transactions + +--- + lib/gs-app.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/lib/gs-app.c b/lib/gs-app.c +index 665eab6c..3dde87c8 100644 +--- a/lib/gs-app.c ++++ b/lib/gs-app.c +@@ -1009,6 +1009,7 @@ gs_app_set_state_internal (GsApp *app, AsAppState state) + case AS_APP_STATE_INSTALLING: + case AS_APP_STATE_REMOVING: + case AS_APP_STATE_QUEUED_FOR_INSTALL: ++ case AS_APP_STATE_PURCHASING: + /* transient, so ignore */ + break; + default: +-- +2.17.0 + diff -Nru gnome-software-3.27.90/debian/patches/0001-snap-Only-load-icons-once.patch gnome-software-3.28.1/debian/patches/0001-snap-Only-load-icons-once.patch --- gnome-software-3.27.90/debian/patches/0001-snap-Only-load-icons-once.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0001-snap-Only-load-icons-once.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,25 @@ +From 3f510b642fd73836a0625bb791e1c6e868bcbe8e Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Mon, 16 Apr 2018 15:24:03 +1200 +Subject: [PATCH] snap: Only load icons once + +--- + plugins/snap/gs-plugin-snap.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/plugins/snap/gs-plugin-snap.c b/plugins/snap/gs-plugin-snap.c +index f2671387..510a02a7 100644 +--- a/plugins/snap/gs-plugin-snap.c ++++ b/plugins/snap/gs-plugin-snap.c +@@ -884,7 +884,7 @@ gs_plugin_refine_app (GsPlugin *plugin, + } + + /* load icon if requested */ +- if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) ++ if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON && gs_app_get_pixbuf (app) == NULL) + load_icon (plugin, client, app, id, local_snap, store_snap, cancellable); + + return TRUE; +-- +2.17.0 + diff -Nru gnome-software-3.27.90/debian/patches/0001-snap-Scale-embedded-snap-icons-to-64x64.patch gnome-software-3.28.1/debian/patches/0001-snap-Scale-embedded-snap-icons-to-64x64.patch --- gnome-software-3.27.90/debian/patches/0001-snap-Scale-embedded-snap-icons-to-64x64.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0001-snap-Scale-embedded-snap-icons-to-64x64.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,50 @@ +From b431417fbb0331324aaa4c14592c6bb869ba6765 Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Mon, 16 Apr 2018 11:26:07 +1200 +Subject: [PATCH] snap: Scale embedded snap icons to 64x64 + +Also take the opportunity to use simpler API to decode these. +--- + plugins/snap/gs-plugin-snap.c | 16 ++++++---------- + 1 file changed, 6 insertions(+), 10 deletions(-) + +diff --git a/plugins/snap/gs-plugin-snap.c b/plugins/snap/gs-plugin-snap.c +index 1c93559e..f2671387 100644 +--- a/plugins/snap/gs-plugin-snap.c ++++ b/plugins/snap/gs-plugin-snap.c +@@ -619,7 +619,8 @@ load_snap_icon (GsApp *app, SnapdClient *client, SnapdSnap *snap, GCancellable * + { + const gchar *icon_url; + g_autoptr(SnapdIcon) icon = NULL; +- g_autoptr(GdkPixbufLoader) loader = NULL; ++ g_autoptr(GInputStream) input_stream = NULL; ++ g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autoptr(GError) error = NULL; + + icon_url = snapd_snap_get_icon (snap); +@@ -632,18 +633,13 @@ load_snap_icon (GsApp *app, SnapdClient *client, SnapdSnap *snap, GCancellable * + return FALSE; + } + +- loader = gdk_pixbuf_loader_new (); +- if (!gdk_pixbuf_loader_write (loader, +- (const guchar *) g_bytes_get_data (snapd_icon_get_data (icon), NULL), +- g_bytes_get_size (snapd_icon_get_data (icon)), &error)) { ++ input_stream = g_memory_input_stream_new_from_bytes (snapd_icon_get_data (icon)); ++ pixbuf = gdk_pixbuf_new_from_stream_at_scale (input_stream, 64, 64, TRUE, cancellable, &error); ++ if (pixbuf == NULL) { + g_warning ("Failed to decode snap icon %s: %s", icon_url, error->message); + return FALSE; + } +- if (!gdk_pixbuf_loader_close (loader, &error)) { +- g_warning ("Failed to decode snap icon %s: %s", icon_url, error->message); +- return FALSE; +- } +- gs_app_set_pixbuf (app, gdk_pixbuf_loader_get_pixbuf (loader)); ++ gs_app_set_pixbuf (app, pixbuf); + + return TRUE; + } +-- +2.17.0 + diff -Nru gnome-software-3.27.90/debian/patches/0002-Download-changelog-information-on-demand-this-stops-.patch gnome-software-3.28.1/debian/patches/0002-Download-changelog-information-on-demand-this-stops-.patch --- gnome-software-3.27.90/debian/patches/0002-Download-changelog-information-on-demand-this-stops-.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0002-Download-changelog-information-on-demand-this-stops-.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,113 @@ +From d14376a2b50d11d159452077a2a90384fb07b1c8 Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Mon, 7 Nov 2016 16:55:18 +1300 +Subject: [PATCH 02/18] Download changelog information on demand - this stops + the UI blocking on startup + +--- + lib/gs-plugin-types.h | 2 ++ + src/gs-update-dialog.c | 40 +++++++++++++++++++++++++++++++++++++--- + 2 files changed, 39 insertions(+), 3 deletions(-) + +diff --git a/lib/gs-plugin-types.h b/lib/gs-plugin-types.h +index 792828f5..b1fa5d68 100644 +--- a/lib/gs-plugin-types.h ++++ b/lib/gs-plugin-types.h +@@ -153,6 +153,7 @@ typedef enum { + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI: Require the origin for UI + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME: Require the runtime + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS: Require screenshot information ++ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANGELOG: Require the changelog + * + * The refine flags. + **/ +@@ -184,6 +185,7 @@ typedef enum { + #define GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI ((guint64) 1 << 24) + #define GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME ((guint64) 1 << 25) + #define GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS ((guint64) 1 << 26) ++#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANGELOG ((guint64) 1 << 27) + typedef guint64 GsPluginRefineFlags; + + /** +diff --git a/src/gs-update-dialog.c b/src/gs-update-dialog.c +index 2509831f..132c475e 100644 +--- a/src/gs-update-dialog.c ++++ b/src/gs-update-dialog.c +@@ -50,6 +50,7 @@ struct _GsUpdateDialog + GQueue *back_entry_stack; + GCancellable *cancellable; + GsPluginLoader *plugin_loader; ++ GsApp *app; + GtkWidget *box_header; + GtkWidget *button_back; + GtkWidget *image_icon; +@@ -96,6 +97,25 @@ back_entry_free (BackEntry *entry) + g_slice_free (BackEntry, entry); + } + ++static void ++refine_cb (GsPluginLoader *plugin_loader, GAsyncResult *res, GsUpdateDialog *dialog) ++{ ++ const gchar *update_details; ++ g_autoptr(GError) error = NULL; ++ ++ if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) ++ g_warning ("Failed to get changelog information: %s", error->message); ++ ++ update_details = gs_app_get_update_details (dialog->app); ++ if (update_details == NULL) { ++ /* TRANSLATORS: this is where the packager did not write ++ * a description for the update */ ++ update_details = _("No update description available."); ++ } ++ ++ gtk_label_set_label (GTK_LABEL (dialog->label_details), update_details); ++} ++ + static void + set_updates_description_ui (GsUpdateDialog *dialog, GsApp *app) + { +@@ -103,6 +123,8 @@ set_updates_description_ui (GsUpdateDialog *dialog, GsApp *app) + const GdkPixbuf *pixbuf; + const gchar *update_details; + ++ g_set_object (&dialog->app, app); ++ + /* set window title */ + kind = gs_app_get_kind (app); + if (kind == AS_APP_KIND_OS_UPDATE) { +@@ -126,9 +148,20 @@ set_updates_description_ui (GsUpdateDialog *dialog, GsApp *app) + gtk_widget_set_visible (dialog->box_header, kind == AS_APP_KIND_DESKTOP); + update_details = gs_app_get_update_details (app); + if (update_details == NULL) { +- /* TRANSLATORS: this is where the packager did not write +- * a description for the update */ +- update_details = _("No update description available."); ++ g_autoptr(GsPluginJob) plugin_job = NULL; ++ ++ /* TRANSLATORS: this is displayed while the changelog is being downloaded */ ++ update_details = _("Downloading change information…"); ++ ++ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, ++ "app", app, ++ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANGELOG, ++ NULL); ++ gs_plugin_loader_job_process_async (dialog->plugin_loader, ++ plugin_job, ++ dialog->cancellable, ++ (GAsyncReadyCallback) refine_cb, ++ dialog); + } + gtk_label_set_label (GTK_LABEL (dialog->label_details), update_details); + gtk_label_set_label (GTK_LABEL (dialog->label_name), gs_app_get_name (app)); +@@ -686,6 +719,7 @@ gs_update_dialog_dispose (GObject *object) + } + + g_clear_object (&dialog->plugin_loader); ++ g_clear_object (&dialog->app); + + G_OBJECT_CLASS (gs_update_dialog_parent_class)->dispose (object); + } +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0003-Sort-snaps-before-other-apps.patch gnome-software-3.28.1/debian/patches/0003-Sort-snaps-before-other-apps.patch --- gnome-software-3.27.90/debian/patches/0003-Sort-snaps-before-other-apps.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0003-Sort-snaps-before-other-apps.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,29 @@ +From 43e3603662ec3a63c0f5d2228174111f23a1ba7f Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Wed, 30 Mar 2016 15:55:47 +1300 +Subject: [PATCH 03/18] Sort snaps before other apps + +--- + src/gs-search-page.c | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/src/gs-search-page.c b/src/gs-search-page.c +index 238fc053..0f457f93 100644 +--- a/src/gs-search-page.c ++++ b/src/gs-search-page.c +@@ -206,6 +206,12 @@ gs_search_page_get_app_sort_key (GsApp *app) + { + GString *key = g_string_sized_new (64); + ++ /* sort snaps before other apps */ ++ if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") == 0) ++ g_string_append (key, "9:"); ++ else ++ g_string_append (key, "1:"); ++ + /* sort apps before runtimes and extensions */ + switch (gs_app_get_kind (app)) { + case AS_APP_KIND_DESKTOP: +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0004-Hide-Kudo-details-since-we-don-t-have-good-data.patch gnome-software-3.28.1/debian/patches/0004-Hide-Kudo-details-since-we-don-t-have-good-data.patch --- gnome-software-3.27.90/debian/patches/0004-Hide-Kudo-details-since-we-don-t-have-good-data.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0004-Hide-Kudo-details-since-we-don-t-have-good-data.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,43 @@ +From 3a30beb21146d5cbf82cfb8734b820deb2d7d1c9 Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Tue, 8 Nov 2016 09:58:05 +1300 +Subject: [PATCH 04/18] Hide Kudo details since we don't have good data + +--- + src/gs-details-page.c | 5 +++-- + src/gs-details-page.ui | 2 +- + 2 files changed, 4 insertions(+), 3 deletions(-) + +diff --git a/src/gs-details-page.c b/src/gs-details-page.c +index b49a432a..67c0b605 100644 +--- a/src/gs-details-page.c ++++ b/src/gs-details-page.c +@@ -1062,9 +1062,10 @@ gs_details_page_refresh_all (GsDetailsPage *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; +diff --git a/src/gs-details-page.ui b/src/gs-details-page.ui +index 1856013f..f6379988 100644 +--- a/src/gs-details-page.ui ++++ b/src/gs-details-page.ui +@@ -611,7 +611,7 @@ + 30 + + +- True ++ False + False + 9 + 12 +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0005-details-Show-an-in-app-notification-when-passed-an-i.patch gnome-software-3.28.1/debian/patches/0005-details-Show-an-in-app-notification-when-passed-an-i.patch --- gnome-software-3.27.90/debian/patches/0005-details-Show-an-in-app-notification-when-passed-an-i.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0005-details-Show-an-in-app-notification-when-passed-an-i.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,186 @@ +From 1ee233415b2c0a8817a7bfd8fb513f2bd5da4423 Mon Sep 17 00:00:00 2001 +From: Iain Lane +Date: Tue, 4 Apr 2017 13:25:33 +0100 +Subject: [PATCH 05/18] details: Show an in-app notification when passed an + invalid file or URL + +Previously there was no feedback when gnome-software was fed a bad URL +on the commandline - show a message. +--- + src/gs-details-page.c | 44 ++++++++++++++++++++++++++++++++++++++++---- + src/gs-shell.c | 12 +----------- + src/gs-shell.h | 13 +++++++++++++ + 3 files changed, 54 insertions(+), 15 deletions(-) + +diff --git a/src/gs-details-page.c b/src/gs-details-page.c +index 67c0b605..0a0bc036 100644 +--- a/src/gs-details-page.c ++++ b/src/gs-details-page.c +@@ -1640,23 +1640,47 @@ set_app (GsDetailsPage *self, GsApp *app) + gs_details_page_app_refine2 (self); + } + ++typedef struct { ++ GsDetailsPage *page; ++ gchar *url; ++} GsDetailsFileHelper; ++ ++static void ++gs_details_page_file_helper_free (GsDetailsFileHelper *helper) ++{ ++ g_object_unref (helper->page); ++ g_free (helper->url); ++ g_free (helper); ++} ++ ++G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsDetailsFileHelper, gs_details_page_file_helper_free); ++ + static void + gs_details_page_file_to_app_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) + { + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); +- GsDetailsPage *self = GS_DETAILS_PAGE (user_data); ++ g_autoptr(GsDetailsFileHelper) helper = user_data; ++ GsDetailsPage *self = helper->page; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GError) error = NULL; ++ g_autofree gchar *msg = NULL; + + list = gs_plugin_loader_job_process_finish (plugin_loader, + res, + &error); + if (list == NULL) { ++ /* TRANSLATORS: This message is shown when opening ++ * gnome-software with a path to an unhandled app, e.g. ++ * /no/such/file */ ++ msg = g_strdup_printf (_("Don't know how to handle ‘%s’"), helper->url); + g_warning ("failed to convert file to GsApp: %s", error->message); + /* go back to the overview */ + gs_shell_change_mode (self->shell, GS_SHELL_MODE_OVERVIEW, NULL, FALSE); ++ gs_shell_show_event_app_notify (self->shell, ++ msg, ++ GS_SHELL_EVENT_BUTTON_NONE); + } else { + GsApp *app = gs_app_list_index (list, 0); + set_app (self, app); +@@ -1669,17 +1693,23 @@ gs_details_page_url_to_app_cb (GObject *source, + gpointer user_data) + { + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); +- GsDetailsPage *self = GS_DETAILS_PAGE (user_data); ++ g_autoptr(GsDetailsFileHelper) helper = user_data; ++ GsDetailsPage *self = helper->page; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GError) error = NULL; ++ g_autofree gchar *msg = NULL; + + list = gs_plugin_loader_job_process_finish (plugin_loader, + res, + &error); + if (list == NULL) { ++ msg = g_strdup_printf (_("Don't know how to handle ‘%s’"), helper->url); + g_warning ("failed to convert URL to GsApp: %s", error->message); + /* go back to the overview */ + gs_shell_change_mode (self->shell, GS_SHELL_MODE_OVERVIEW, NULL, FALSE); ++ gs_shell_show_event_app_notify (self->shell, ++ msg, ++ GS_SHELL_EVENT_BUTTON_NONE); + } else { + GsApp *app = gs_app_list_index (list, 0); + set_app (self, app); +@@ -1690,6 +1720,9 @@ void + gs_details_page_set_local_file (GsDetailsPage *self, GFile *file) + { + g_autoptr(GsPluginJob) plugin_job = NULL; ++ GsDetailsFileHelper *helper = g_new0 (GsDetailsFileHelper, 1); ++ helper->page = g_object_ref (self); ++ helper->url = g_file_get_uri (file); + gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_LOADING); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, +@@ -1712,13 +1745,16 @@ gs_details_page_set_local_file (GsDetailsPage *self, GFile *file) + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + gs_details_page_file_to_app_cb, +- self); ++ helper); + } + + void + gs_details_page_set_url (GsDetailsPage *self, const gchar *url) + { + g_autoptr(GsPluginJob) plugin_job = NULL; ++ GsDetailsFileHelper *helper = g_new0 (GsDetailsFileHelper, 1); ++ helper->page = g_object_ref (self); ++ helper->url = g_strdup (url); + gs_details_page_set_state (self, GS_DETAILS_PAGE_STATE_LOADING); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_URL_TO_APP, + "search", url, +@@ -1742,7 +1778,7 @@ gs_details_page_set_url (GsDetailsPage *self, const gchar *url) + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, + gs_details_page_url_to_app_cb, +- self); ++ helper); + } + + static void +diff --git a/src/gs-shell.c b/src/gs-shell.c +index d6121b33..89c82241 100644 +--- a/src/gs-shell.c ++++ b/src/gs-shell.c +@@ -771,17 +771,7 @@ gs_shell_allow_updates_notify_cb (GsPluginLoader *plugin_loader, + priv->mode == GS_SHELL_MODE_UPDATES); + } + +-typedef enum { +- GS_SHELL_EVENT_BUTTON_NONE = 0, +- GS_SHELL_EVENT_BUTTON_SOURCES = 1 << 0, +- GS_SHELL_EVENT_BUTTON_NO_SPACE = 1 << 1, +- GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS = 1 << 2, +- GS_SHELL_EVENT_BUTTON_MORE_INFO = 1 << 3, +- GS_SHELL_EVENT_BUTTON_RESTART_REQUIRED = 1 << 4, +- GS_SHELL_EVENT_BUTTON_LAST +-} GsShellEventButtons; +- +-static void ++void + gs_shell_show_event_app_notify (GsShell *shell, + const gchar *title, + GsShellEventButtons buttons) +diff --git a/src/gs-shell.h b/src/gs-shell.h +index 03af8e61..9b23f851 100644 +--- a/src/gs-shell.h ++++ b/src/gs-shell.h +@@ -40,6 +40,16 @@ struct _GsShellClass + void (* loaded) (GsShell *shell); + }; + ++typedef enum { ++ GS_SHELL_EVENT_BUTTON_NONE = 0, ++ GS_SHELL_EVENT_BUTTON_SOURCES = 1 << 0, ++ GS_SHELL_EVENT_BUTTON_NO_SPACE = 1 << 1, ++ GS_SHELL_EVENT_BUTTON_NETWORK_SETTINGS = 1 << 2, ++ GS_SHELL_EVENT_BUTTON_MORE_INFO = 1 << 3, ++ GS_SHELL_EVENT_BUTTON_RESTART_REQUIRED = 1 << 4, ++ GS_SHELL_EVENT_BUTTON_LAST ++} GsShellEventButtons; ++ + typedef enum { + GS_SHELL_MODE_UNKNOWN, + GS_SHELL_MODE_OVERVIEW, +@@ -99,6 +109,9 @@ void gs_shell_show_extras_search (GsShell *shell, + gchar **resources); + void gs_shell_show_uri (GsShell *shell, + const gchar *url); ++void gs_shell_show_event_app_notify (GsShell *shell, ++ const gchar *title, ++ GsShellEventButtons buttons); + void gs_shell_setup (GsShell *shell, + GsPluginLoader *plugin_loader, + GCancellable *cancellable); +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0006-packagekit-Disable-updates.patch gnome-software-3.28.1/debian/patches/0006-packagekit-Disable-updates.patch --- gnome-software-3.27.90/debian/patches/0006-packagekit-Disable-updates.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0006-packagekit-Disable-updates.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,28 @@ +From 92ac44de416483336b44a12cb7c9923c5def597a Mon Sep 17 00:00:00 2001 +From: Iain Lane +Date: Thu, 20 Jul 2017 10:12:58 +0100 +Subject: [PATCH 06/18] packagekit: Disable updates + +In Ubuntu we use Update Manager for this currently. The long term plan +is to use Software, but we're not there yet. +--- + plugins/packagekit/gs-plugin-systemd-updates.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/plugins/packagekit/gs-plugin-systemd-updates.c b/plugins/packagekit/gs-plugin-systemd-updates.c +index 7c6f5d95..766c00b6 100644 +--- a/plugins/packagekit/gs-plugin-systemd-updates.c ++++ b/plugins/packagekit/gs-plugin-systemd-updates.c +@@ -153,6 +153,9 @@ gs_plugin_add_updates (GsPlugin *plugin, + g_autoptr(GError) error_local = NULL; + g_auto(GStrv) package_ids = NULL; + ++ /* In Ubuntu we're using Update Manager for this, for now. */ ++ return TRUE; ++ + /* get the id's if the file exists */ + package_ids = pk_offline_get_prepared_ids (&error_local); + if (package_ids == NULL) { +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0007-snap-Only-feature-snaps.patch gnome-software-3.28.1/debian/patches/0007-snap-Only-feature-snaps.patch --- gnome-software-3.27.90/debian/patches/0007-snap-Only-feature-snaps.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0007-snap-Only-feature-snaps.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,65 @@ +From 5a4ea66dda045e6e9643eec0e75bf2df814a4a7e Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Wed, 9 Aug 2017 15:43:02 +1200 +Subject: [PATCH 07/18] snap: Only feature snaps + +Run after the other plugins that populate featured/popular apps and remove +them when we set ours. +--- + plugins/snap/gs-plugin-snap.c | 15 ++++++++++++++- + 1 file changed, 14 insertions(+), 1 deletion(-) + +diff --git a/plugins/snap/gs-plugin-snap.c b/plugins/snap/gs-plugin-snap.c +index 1c93559e..cb8cc611 100644 +--- a/plugins/snap/gs-plugin-snap.c ++++ b/plugins/snap/gs-plugin-snap.c +@@ -84,11 +84,13 @@ gs_plugin_initialize (GsPlugin *plugin) + + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "desktop-categories"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "ubuntu-reviews"); ++ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "appstream"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_BETTER_THAN, "packagekit"); + gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons"); + + /* Override hardcoded popular apps */ +- gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "hardcoded-popular"); ++ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "hardcoded-popular"); ++ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "hardcoded-featured"); + + /* set name of MetaInfo file */ + gs_plugin_set_appstream_id (plugin, "org.gnome.Software.Plugin.Snap"); +@@ -389,6 +391,12 @@ is_banner_icon_image (const gchar *filename) + return g_regex_match_simple ("^banner-icon(?:_[a-zA-Z0-9]{7})?\\.(?:png|jpg)$", filename, 0, 0); + } + ++static gboolean ++remove_cb (GsApp *app, gpointer user_data) ++{ ++ return FALSE; ++} ++ + gboolean + gs_plugin_add_featured (GsPlugin *plugin, + GsAppList *list, +@@ -457,6 +465,8 @@ gs_plugin_add_featured (GsPlugin *plugin, + background_css->str); + gs_app_set_metadata (app, "GnomeSoftware::FeatureTile-css", css); + ++ /* replace any other featured apps with our one */ ++ gs_app_list_filter (list, remove_cb, NULL); + gs_app_list_add (list, app); + + return TRUE; +@@ -475,6 +485,9 @@ gs_plugin_add_popular (GsPlugin *plugin, + if (snaps == NULL) + return FALSE; + ++ /* replace any other popular apps with our one */ ++ gs_app_list_filter (list, remove_cb, NULL); ++ + /* skip first snap - it is used as the featured app */ + for (i = 1; i < snaps->len; i++) { + g_autoptr(GsApp) app = snap_to_app (plugin, g_ptr_array_index (snaps, i)); +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0008-Don-t-randomize-editors-picks.patch gnome-software-3.28.1/debian/patches/0008-Don-t-randomize-editors-picks.patch --- gnome-software-3.27.90/debian/patches/0008-Don-t-randomize-editors-picks.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0008-Don-t-randomize-editors-picks.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,29 @@ +From 4d164ba4017396a0de288c3cdd5ab8f3df9a98a2 Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Fri, 18 Aug 2017 16:27:26 +1200 +Subject: [PATCH 08/18] Don't randomize editors picks. + +The featured snaps are provided in a significant order. +https://bugs.launchpad.net/bugs/1705953 +--- + src/gs-overview-page.c | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/src/gs-overview-page.c b/src/gs-overview-page.c +index cbb45ef3..81c8612c 100644 +--- a/src/gs-overview-page.c ++++ b/src/gs-overview-page.c +@@ -187,7 +187,9 @@ gs_overview_page_get_popular_cb (GObject *source_object, + + /* Don't show apps from the category that's currently featured as the category of the day */ + gs_app_list_filter (list, filter_category, priv->category_of_day); +- gs_app_list_randomize (list); ++ /* Disabled on Ubuntu as we only show snaps and the order is significant ++ * https://bugs.launchpad.net/bugs/1705953 */ ++ //gs_app_list_randomize (list); + + gs_container_remove_all (GTK_CONTAINER (priv->box_popular)); + +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0009-Display-a-warning-for-non-sandboxed-snaps.patch gnome-software-3.28.1/debian/patches/0009-Display-a-warning-for-non-sandboxed-snaps.patch --- gnome-software-3.27.90/debian/patches/0009-Display-a-warning-for-non-sandboxed-snaps.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0009-Display-a-warning-for-non-sandboxed-snaps.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,121 @@ +From 45898c1500863287904c9463b1cf4fb7c7e363eb Mon Sep 17 00:00:00 2001 +From: James Henstridge +Date: Fri, 23 Jun 2017 11:22:36 +0800 +Subject: [PATCH 09/18] Display a warning for non-sandboxed snaps. + +--- + src/gs-details-page.c | 14 ++++++++++++++ + src/gs-details-page.ui | 44 ++++++++++++++++++++++++++++++++++++++++++-- + 2 files changed, 56 insertions(+), 2 deletions(-) + +diff --git a/src/gs-details-page.c b/src/gs-details-page.c +index 0a0bc036..5380541c 100644 +--- a/src/gs-details-page.c ++++ b/src/gs-details-page.c +@@ -145,6 +145,7 @@ struct _GsDetailsPage + GtkWidget *label_content_rating_none; + GtkWidget *button_details_rating_value; + GtkWidget *label_details_rating_title; ++ GtkWidget *box_not_sandboxed_warning; + }; + + G_DEFINE_TYPE (GsDetailsPage, gs_details_page, GS_TYPE_PAGE) +@@ -1071,6 +1072,18 @@ gs_details_page_refresh_all (GsDetailsPage *self) + break; + } + ++ /* Display a warning about non-sandboxed apps that may come ++ * from third party sources. Currently only checking snaps. */ ++ ret = FALSE; ++ switch (gs_app_get_bundle_kind (self->app)) { ++ case AS_BUNDLE_KIND_SNAP: ++ ret |= (kudos & GS_APP_KUDO_SANDBOXED) == 0; ++ break; ++ default: ++ break; ++ } ++ gtk_widget_set_visible (self->box_not_sandboxed_warning, ret); ++ + /* are we trying to replace something in the baseos */ + gtk_widget_set_visible (self->infobar_details_package_baseos, + gs_app_has_quirk (self->app, AS_APP_QUIRK_COMPULSORY) && +@@ -2517,6 +2530,7 @@ gs_details_page_class_init (GsDetailsPageClass *klass) + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_content_rating_none); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_rating_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_rating_title); ++ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_not_sandboxed_warning); + } + + static void +diff --git a/src/gs-details-page.ui b/src/gs-details-page.ui +index f6379988..1943af50 100644 +--- a/src/gs-details-page.ui ++++ b/src/gs-details-page.ui +@@ -603,6 +603,46 @@ + 11 + + ++ ++ ++ False ++ False ++ 30 ++ ++ ++ True ++ False ++ 16 ++ dialog-warning ++ 6 ++ ++ ++ False ++ True ++ 0 ++ ++ ++ ++ ++ True ++ False ++ This application is unconfined. It can access all personal files and system resources. ++ 0 ++ 0.5 ++ ++ ++ True ++ True ++ 1 ++ ++ ++ ++ ++ True ++ True ++ 12 ++ ++ + + + True +@@ -1138,7 +1178,7 @@ + + False + True +- 12 ++ 13 + + + +@@ -1201,7 +1241,7 @@ + + False + True +- 13 ++ 14 + + + +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0010-Sort-category-snaps-before-other-packages.patch gnome-software-3.28.1/debian/patches/0010-Sort-category-snaps-before-other-packages.patch --- gnome-software-3.27.90/debian/patches/0010-Sort-category-snaps-before-other-packages.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0010-Sort-category-snaps-before-other-packages.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,41 @@ +From 4b3ec93ac66b2cbc4095a6b3b9072fb883344ff6 Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Mon, 22 Jan 2018 16:50:29 +1300 +Subject: [PATCH 10/18] Sort category snaps before other packages + +--- + src/gs-category-page.c | 11 +++++++++-- + 1 file changed, 9 insertions(+), 2 deletions(-) + +diff --git a/src/gs-category-page.c b/src/gs-category-page.c +index 2ae8781b..4a1bf88b 100644 +--- a/src/gs-category-page.c ++++ b/src/gs-category-page.c +@@ -191,6 +191,8 @@ gs_category_page_get_apps_cb (GObject *source_object, + static gboolean + _max_results_sort_cb (GsApp *app1, GsApp *app2, gpointer user_data) + { ++ if (gs_app_get_bundle_kind (app1) == AS_BUNDLE_KIND_SNAP || gs_app_get_bundle_kind (app2) == AS_BUNDLE_KIND_SNAP) ++ return gs_app_get_bundle_kind (app1) == AS_BUNDLE_KIND_SNAP ? -1 : 1; + return gs_app_get_rating (app1) < gs_app_get_rating (app2); + } + +@@ -211,8 +213,13 @@ gs_category_page_sort_flow_box_sort_func (GtkFlowBoxChild *child1, + sort_type = GS_CATEGORY_PAGE (data)->sort_type; + + if (sort_type == SUBCATEGORY_SORT_TYPE_RATING) { +- gint rating_app1 = gs_app_get_rating (app1); +- gint rating_app2 = gs_app_get_rating (app2); ++ gint rating_app1, rating_app2; ++ ++ if (gs_app_get_bundle_kind (app1) == AS_BUNDLE_KIND_SNAP || gs_app_get_bundle_kind (app2) == AS_BUNDLE_KIND_SNAP) ++ return gs_app_get_bundle_kind (app1) == AS_BUNDLE_KIND_SNAP ? -1 : 1; ++ ++ rating_app1 = gs_app_get_rating (app1); ++ rating_app2 = gs_app_get_rating (app2); + if (rating_app1 > rating_app2) + return -1; + if (rating_app1 < rating_app2) +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0011-Support-snap-channels.patch gnome-software-3.28.1/debian/patches/0011-Support-snap-channels.patch --- gnome-software-3.27.90/debian/patches/0011-Support-snap-channels.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0011-Support-snap-channels.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,1710 @@ +From 7ec4f5afe0c06a15a247cd7be883d81754a09bea Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Thu, 23 Nov 2017 10:54:20 +1300 +Subject: [PATCH 11/18] Support snap channels + +--- + lib/gs-app.c | 106 +++++++++++++++++++ + lib/gs-app.h | 9 ++ + lib/gs-channel.c | 116 +++++++++++++++++++++ + lib/gs-channel.h | 44 ++++++++ + lib/gs-plugin-job-private.h | 1 + + lib/gs-plugin-job.c | 31 ++++++ + lib/gs-plugin-job.h | 2 + + lib/gs-plugin-loader.c | 13 +++ + lib/gs-plugin-types.h | 4 + + lib/gs-plugin-vfuncs.h | 18 ++++ + lib/gs-plugin.c | 6 ++ + lib/gs-self-test.c | 16 +++ + lib/gs-utils.c | 35 +++++++ + lib/gs-utils.h | 2 + + lib/meson.build | 2 + + meson.build | 2 +- + plugins/dummy/gs-plugin-dummy.c | 11 ++ + plugins/snap/gs-plugin-snap.c | 226 +++++++++++++++++++++++++++++++++++----- + src/gs-details-page.c | 183 +++++++++++++++++++++++++++++++- + src/gs-details-page.ui | 94 +++++++++++++---- + src/gtk-style.css | 31 ++++++ + 21 files changed, 907 insertions(+), 45 deletions(-) + create mode 100644 lib/gs-channel.c + create mode 100644 lib/gs-channel.h + +diff --git a/lib/gs-app.c b/lib/gs-app.c +index 264095ff..02b94228 100644 +--- a/lib/gs-app.c ++++ b/lib/gs-app.c +@@ -127,6 +127,8 @@ typedef struct + AsContentRating *content_rating; + GdkPixbuf *pixbuf; + GsPrice *price; ++ GPtrArray *channels; ++ GsChannel *active_channel; + GCancellable *cancellable; + GsPluginAction pending_action; + } GsAppPrivate; +@@ -596,6 +598,18 @@ gs_app_to_string_append (GsApp *app, GString *str) + gs_app_kv_lpad (str, "keyword", tmp); + } + } ++ for (i = 0; i < priv->channels->len; i++) { ++ GsChannel *channel = g_ptr_array_index (priv->channels, i); ++ g_autofree gchar *key = NULL; ++ key = g_strdup_printf ("channel-%02u", i); ++ gs_app_kv_printf (str, key, "%s [%s]", ++ gs_channel_get_name (channel), ++ gs_channel_get_version (channel)); ++ } ++ if (priv->active_channel != NULL) { ++ gs_app_kv_printf (str, "active-channel", "%s", ++ gs_channel_get_name (priv->active_channel)); ++ } + keys = g_hash_table_get_keys (priv->metadata); + for (GList *l = keys; l != NULL; l = l->next) { + GVariant *val; +@@ -3725,6 +3739,23 @@ gs_app_add_kudo (GsApp *app, GsAppKudo kudo) + priv->kudos |= kudo; + } + ++/** ++ * gs_app_remove_kudo: ++ * @app: a #GsApp ++ * @kudo: a #GsAppKudo, e.g. %GS_APP_KUDO_MY_LANGUAGE ++ * ++ * Removes a kudo from an application. ++ * ++ * Since: 3.28 ++ **/ ++void ++gs_app_remove_kudo (GsApp *app, GsAppKudo kudo) ++{ ++ GsAppPrivate *priv = gs_app_get_instance_private (app); ++ g_return_if_fail (GS_IS_APP (app)); ++ priv->kudos &= ~kudo; ++} ++ + /** + * gs_app_has_kudo: + * @app: a #GsApp +@@ -4000,6 +4031,78 @@ gs_app_get_priority (GsApp *app) + return priv->priority; + } + ++/** ++ * gs_app_add_channel: ++ * @app: a #GsApp ++ * @channel: a #GsChannel ++ * ++ * Adds a channel to the application. ++ * ++ * Since: 3.28 ++ **/ ++void ++gs_app_add_channel (GsApp *app, GsChannel *channel) ++{ ++ GsAppPrivate *priv = gs_app_get_instance_private (app); ++ g_return_if_fail (GS_IS_APP (app)); ++ g_return_if_fail (GS_IS_CHANNEL (channel)); ++ g_ptr_array_add (priv->channels, g_object_ref (channel)); ++} ++ ++/** ++ * gs_app_get_channels: ++ * @app: a #GsApp ++ * ++ * Gets the list of channels. ++ * ++ * Returns: (element-type GsChannel) (transfer none): a list ++ * ++ * Since: 3.28 ++ **/ ++GPtrArray * ++gs_app_get_channels (GsApp *app) ++{ ++ GsAppPrivate *priv = gs_app_get_instance_private (app); ++ g_return_val_if_fail (GS_IS_APP (app), NULL); ++ return priv->channels; ++} ++ ++/** ++ * gs_app_set_active_channel: ++ * @app: a #GsApp ++ * @channel: a #GsChannel ++ * ++ * Set the currently active channel. ++ * ++ * Since: 3.28 ++ **/ ++void ++gs_app_set_active_channel (GsApp *app, GsChannel *channel) ++{ ++ GsAppPrivate *priv = gs_app_get_instance_private (app); ++ g_return_if_fail (GS_IS_APP (app)); ++ g_return_if_fail (GS_IS_CHANNEL (channel)); ++ g_set_object (&priv->active_channel, channel); ++} ++ ++/** ++ * gs_app_get_active_channel: ++ * @app: a #GsApp ++ * ++ * Gets the currently active channel. ++ * ++ * Returns: a #GsChannel or %NULL. ++ * ++ * Since: 3.28 ++ **/ ++GsChannel * ++gs_app_get_active_channel (GsApp *app) ++{ ++ GsAppPrivate *priv = gs_app_get_instance_private (app); ++ g_return_val_if_fail (GS_IS_APP (app), NULL); ++ return priv->active_channel; ++} ++ + /** + * gs_app_get_cancellable: + * @app: a #GsApp +@@ -4182,6 +4285,8 @@ gs_app_dispose (GObject *object) + g_clear_pointer (&priv->reviews, g_ptr_array_unref); + g_clear_pointer (&priv->provides, g_ptr_array_unref); + g_clear_pointer (&priv->icons, g_ptr_array_unref); ++ g_clear_pointer (&priv->channels, g_ptr_array_unref); ++ g_clear_object (&priv->active_channel); + + G_OBJECT_CLASS (gs_app_parent_class)->dispose (object); + } +@@ -4370,6 +4475,7 @@ gs_app_init (GsApp *app) + priv->reviews = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->provides = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->icons = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); ++ priv->channels = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->metadata = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, +diff --git a/lib/gs-app.h b/lib/gs-app.h +index 2d7e6d06..1770909e 100644 +--- a/lib/gs-app.h ++++ b/lib/gs-app.h +@@ -28,6 +28,7 @@ + #include + #include + ++#include "gs-channel.h" + #include "gs-price.h" + + G_BEGIN_DECLS +@@ -307,6 +308,8 @@ void gs_app_set_keywords (GsApp *app, + GPtrArray *keywords); + void gs_app_add_kudo (GsApp *app, + GsAppKudo kudo); ++void gs_app_remove_kudo (GsApp *app, ++ GsAppKudo kudo); + gboolean gs_app_has_kudo (GsApp *app, + GsAppKudo kudo); + guint64 gs_app_get_kudos (GsApp *app); +@@ -326,6 +329,12 @@ void gs_app_remove_quirk (GsApp *app, + AsAppQuirk quirk); + gboolean gs_app_is_installed (GsApp *app); + gboolean gs_app_is_updatable (GsApp *app); ++GPtrArray *gs_app_get_channels (GsApp *app); ++void gs_app_add_channel (GsApp *app, ++ GsChannel *channel); ++void gs_app_set_active_channel (GsApp *app, ++ GsChannel *channel); ++GsChannel *gs_app_get_active_channel (GsApp *app); + G_END_DECLS + + #endif /* __GS_APP_H */ +diff --git a/lib/gs-channel.c b/lib/gs-channel.c +new file mode 100644 +index 00000000..658a33b0 +--- /dev/null ++++ b/lib/gs-channel.c +@@ -0,0 +1,116 @@ ++/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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 ++ ++#include "gs-channel.h" ++ ++struct _GsChannel ++{ ++ GObject parent_instance; ++ ++ gchar *name; ++ gchar *version; ++}; ++ ++G_DEFINE_TYPE (GsChannel, gs_channel, G_TYPE_OBJECT) ++ ++/** ++ * gs_channel_get_name: ++ * @channel: a #GsChannel ++ * ++ * Get the channel name. ++ * ++ * Returns: a channel name. ++ * ++ * Since: 3.28 ++ */ ++const gchar * ++gs_channel_get_name (GsChannel *channel) ++{ ++ g_return_val_if_fail (GS_IS_CHANNEL (channel), NULL); ++ return channel->name; ++} ++ ++/** ++ * gs_channel_get_version: ++ * @channel: a #GsChannel ++ * ++ * Get the channel version. ++ * ++ * Returns: a channel version. ++ * ++ * Since: 3.28 ++ */ ++const gchar * ++gs_channel_get_version (GsChannel *channel) ++{ ++ g_return_val_if_fail (GS_IS_CHANNEL (channel), NULL); ++ return channel->version; ++} ++ ++static void ++gs_channel_finalize (GObject *object) ++{ ++ GsChannel *channel = GS_CHANNEL (object); ++ ++ g_free (channel->name); ++ g_free (channel->version); ++ ++ G_OBJECT_CLASS (gs_channel_parent_class)->finalize (object); ++} ++ ++static void ++gs_channel_class_init (GsChannelClass *klass) ++{ ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ object_class->finalize = gs_channel_finalize; ++} ++ ++static void ++gs_channel_init (GsChannel *channel) ++{ ++} ++ ++/** ++ * gs_channel_new: ++ * @name: the name of the channel. ++ * @version: the version this channel is providing. ++ * ++ * Creates a new channel object. ++ * ++ * Return value: a new #GsChannel object. ++ * ++ * Since: 3.28 ++ **/ ++GsChannel * ++gs_channel_new (const gchar *name, const gchar *version) ++{ ++ GsChannel *channel; ++ channel = g_object_new (GS_TYPE_CHANNEL, NULL); ++ channel->name = g_strdup (name); ++ channel->version = g_strdup (version); ++ return GS_CHANNEL (channel); ++} ++ ++/* vim: set noexpandtab: */ +diff --git a/lib/gs-channel.h b/lib/gs-channel.h +new file mode 100644 +index 00000000..64610ab6 +--- /dev/null ++++ b/lib/gs-channel.h +@@ -0,0 +1,44 @@ ++ /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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_CHANNEL_H ++#define __GS_CHANNEL_H ++ ++#include ++ ++G_BEGIN_DECLS ++ ++#define GS_TYPE_CHANNEL (gs_channel_get_type ()) ++ ++G_DECLARE_FINAL_TYPE (GsChannel, gs_channel, GS, CHANNEL, GObject) ++ ++GsChannel *gs_channel_new (const gchar *name, ++ const gchar *version); ++ ++const gchar *gs_channel_get_name (GsChannel *channel); ++ ++const gchar *gs_channel_get_version (GsChannel *channel); ++ ++G_END_DECLS ++ ++#endif /* __GS_CHANNEL_H */ ++ ++/* vim: set noexpandtab: */ +diff --git a/lib/gs-plugin-job-private.h b/lib/gs-plugin-job-private.h +index f238213f..414414da 100644 +--- a/lib/gs-plugin-job-private.h ++++ b/lib/gs-plugin-job-private.h +@@ -53,6 +53,7 @@ GsPlugin *gs_plugin_job_get_plugin (GsPluginJob *self); + GsCategory *gs_plugin_job_get_category (GsPluginJob *self); + AsReview *gs_plugin_job_get_review (GsPluginJob *self); + GsPrice *gs_plugin_job_get_price (GsPluginJob *self); ++GsChannel *gs_plugin_job_get_channel (GsPluginJob *self); + gchar *gs_plugin_job_to_string (GsPluginJob *self); + void gs_plugin_job_set_action (GsPluginJob *self, + GsPluginAction action); +diff --git a/lib/gs-plugin-job.c b/lib/gs-plugin-job.c +index 13fbcacc..cf9ab6ca 100644 +--- a/lib/gs-plugin-job.c ++++ b/lib/gs-plugin-job.c +@@ -49,6 +49,7 @@ struct _GsPluginJob + GsCategory *category; + AsReview *review; + GsPrice *price; ++ GsChannel *channel; + gint64 time_created; + }; + +@@ -69,6 +70,7 @@ enum { + PROP_REVIEW, + PROP_MAX_RESULTS, + PROP_PRICE, ++ PROP_CHANNEL, + PROP_TIMEOUT, + PROP_LAST + }; +@@ -129,6 +131,9 @@ gs_plugin_job_to_string (GsPluginJob *self) + g_autofree gchar *price_string = gs_price_to_string (self->price); + g_string_append_printf (str, " with price=%s", price_string); + } ++ if (self->channel != NULL) { ++ g_string_append_printf (str, " with channel=%s", gs_channel_get_name (self->channel)); ++ } + if (self->auth != NULL) { + g_string_append_printf (str, " with auth=%s", + gs_auth_get_provider_id (self->auth)); +@@ -453,6 +458,20 @@ gs_plugin_job_get_price (GsPluginJob *self) + return self->price; + } + ++void ++gs_plugin_job_set_channel (GsPluginJob *self, GsChannel *channel) ++{ ++ g_return_if_fail (GS_IS_PLUGIN_JOB (self)); ++ g_set_object (&self->channel, channel); ++} ++ ++GsChannel * ++gs_plugin_job_get_channel (GsPluginJob *self) ++{ ++ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); ++ return self->channel; ++} ++ + static void + gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) + { +@@ -501,6 +520,9 @@ gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSp + case PROP_PRICE: + g_value_set_object (value, self->price); + break; ++ case PROP_CHANNEL: ++ g_value_set_object (value, self->channel); ++ break; + case PROP_MAX_RESULTS: + g_value_set_uint (value, self->max_results); + break; +@@ -567,6 +589,9 @@ gs_plugin_job_set_property (GObject *obj, guint prop_id, const GValue *value, GP + case PROP_PRICE: + gs_plugin_job_set_price (self, g_value_get_object (value)); + break; ++ case PROP_CHANNEL: ++ gs_plugin_job_set_channel (self, g_value_get_object (value)); ++ break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; +@@ -586,6 +611,7 @@ gs_plugin_job_finalize (GObject *obj) + g_clear_object (&self->category); + g_clear_object (&self->review); + g_clear_object (&self->price); ++ g_clear_object (&self->channel); + G_OBJECT_CLASS (gs_plugin_job_parent_class)->finalize (obj); + } + +@@ -679,6 +705,11 @@ gs_plugin_job_class_init (GsPluginJobClass *klass) + GS_TYPE_PRICE, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_PRICE, pspec); ++ ++ pspec = g_param_spec_object ("channel", NULL, NULL, ++ GS_TYPE_CHANNEL, ++ G_PARAM_READWRITE); ++ g_object_class_install_property (object_class, PROP_CHANNEL, pspec); + } + + static void +diff --git a/lib/gs-plugin-job.h b/lib/gs-plugin-job.h +index 2edd2851..48c3fa39 100644 +--- a/lib/gs-plugin-job.h ++++ b/lib/gs-plugin-job.h +@@ -72,6 +72,8 @@ void gs_plugin_job_set_review (GsPluginJob *self, + AsReview *review); + void gs_plugin_job_set_price (GsPluginJob *self, + GsPrice *price); ++void gs_plugin_job_set_channel (GsPluginJob *self, ++ GsChannel *channel); + + #define gs_plugin_job_newv(a,...) GS_PLUGIN_JOB(g_object_new(GS_TYPE_PLUGIN_JOB, "action", a, __VA_ARGS__)) + +diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c +index be4e29c2..af69ea2c 100644 +--- a/lib/gs-plugin-loader.c ++++ b/lib/gs-plugin-loader.c +@@ -133,6 +133,11 @@ typedef gboolean (*GsPluginPurchaseFunc) (GsPlugin *plugin, + GsPrice *price, + GCancellable *cancellable, + GError **error); ++typedef gboolean (*GsPluginSwitchChannelFunc) (GsPlugin *plugin, ++ GsApp *app, ++ GsChannel *channel, ++ GCancellable *cancellable, ++ GError **error); + typedef gboolean (*GsPluginReviewFunc) (GsPlugin *plugin, + GsApp *app, + AsReview *review, +@@ -637,6 +642,14 @@ gs_plugin_loader_call_vfunc (GsPluginLoaderHelper *helper, + cancellable, &error_local); + } + break; ++ case GS_PLUGIN_ACTION_SWITCH_CHANNEL: ++ { ++ GsPluginSwitchChannelFunc plugin_func = func; ++ ret = plugin_func (plugin, app, ++ gs_plugin_job_get_channel (helper->plugin_job), ++ cancellable, &error_local); ++ } ++ break; + case GS_PLUGIN_ACTION_REVIEW_SUBMIT: + case GS_PLUGIN_ACTION_REVIEW_UPVOTE: + case GS_PLUGIN_ACTION_REVIEW_DOWNVOTE: +diff --git a/lib/gs-plugin-types.h b/lib/gs-plugin-types.h +index b1fa5d68..38f13290 100644 +--- a/lib/gs-plugin-types.h ++++ b/lib/gs-plugin-types.h +@@ -154,6 +154,7 @@ typedef enum { + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME: Require the runtime + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS: Require screenshot information + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANGELOG: Require the changelog ++ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANNELS: Require channel information + * + * The refine flags. + **/ +@@ -186,6 +187,7 @@ typedef enum { + #define GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME ((guint64) 1 << 25) + #define GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS ((guint64) 1 << 26) + #define GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANGELOG ((guint64) 1 << 27) ++#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANNELS ((guint64) 1 << 28) + typedef guint64 GsPluginRefineFlags; + + /** +@@ -273,6 +275,7 @@ typedef enum { + * @GS_PLUGIN_ACTION_INITIALIZE: Initialize the plugin + * @GS_PLUGIN_ACTION_DESTROY: Destroy the plugin + * @GS_PLUGIN_ACTION_PURCHASE: Purchase an app ++ * @GS_PLUGIN_ACTION_SWITCH_CHANNEL: Switch app channel + * + * The plugin action. + **/ +@@ -320,6 +323,7 @@ typedef enum { + GS_PLUGIN_ACTION_INITIALIZE, + GS_PLUGIN_ACTION_DESTROY, + GS_PLUGIN_ACTION_PURCHASE, ++ GS_PLUGIN_ACTION_SWITCH_CHANNEL, + /*< private >*/ + GS_PLUGIN_ACTION_LAST + } GsPluginAction; +diff --git a/lib/gs-plugin-vfuncs.h b/lib/gs-plugin-vfuncs.h +index f8d48e7a..61447150 100644 +--- a/lib/gs-plugin-vfuncs.h ++++ b/lib/gs-plugin-vfuncs.h +@@ -611,6 +611,24 @@ gboolean gs_plugin_app_install (GsPlugin *plugin, + GCancellable *cancellable, + GError **error); + ++/** ++ * gs_plugin_app_switch_channel: ++ * @plugin: a #GsPlugin ++ * @app: a #GsApp ++ * @channel: a #GsChannel ++ * @cancellable: a #GCancellable, or %NULL ++ * @error: a #GError, or %NULL ++ * ++ * Set the app chanel. ++ * ++ * Returns: %TRUE for success or if not relevant ++ **/ ++gboolean gs_plugin_app_switch_channel (GsPlugin *plugin, ++ GsApp *app, ++ GsChannel *channel, ++ GCancellable *cancellable, ++ GError **error); ++ + /** + * gs_plugin_app_remove: + * @plugin: a #GsPlugin +diff --git a/lib/gs-plugin.c b/lib/gs-plugin.c +index 838057a9..f6e7a3f1 100644 +--- a/lib/gs-plugin.c ++++ b/lib/gs-plugin.c +@@ -1769,6 +1769,8 @@ gs_plugin_action_to_function_name (GsPluginAction action) + return "gs_plugin_destroy"; + if (action == GS_PLUGIN_ACTION_PURCHASE) + return "gs_plugin_app_purchase"; ++ if (action == GS_PLUGIN_ACTION_SWITCH_CHANNEL) ++ return "gs_plugin_app_switch_channel"; + return NULL; + } + +@@ -1869,6 +1871,8 @@ gs_plugin_action_to_string (GsPluginAction action) + return "destroy"; + if (action == GS_PLUGIN_ACTION_PURCHASE) + return "purchase"; ++ if (action == GS_PLUGIN_ACTION_SWITCH_CHANNEL) ++ return "switch-channel"; + return NULL; + } + +@@ -1969,6 +1973,8 @@ gs_plugin_action_from_string (const gchar *action) + return GS_PLUGIN_ACTION_DESTROY; + if (g_strcmp0 (action, "purchase") == 0) + return GS_PLUGIN_ACTION_PURCHASE; ++ if (g_strcmp0 (action, "switch-channel") == 0) ++ return GS_PLUGIN_ACTION_SWITCH_CHANNEL; + return GS_PLUGIN_ACTION_UNKNOWN; + } + +diff --git a/lib/gs-self-test.c b/lib/gs-self-test.c +index c6d9dad6..2b17453f 100644 +--- a/lib/gs-self-test.c ++++ b/lib/gs-self-test.c +@@ -44,6 +44,11 @@ gs_utils_url_func (void) + g_autofree gchar *path3 = NULL; + g_autofree gchar *scheme1 = NULL; + g_autofree gchar *scheme2 = NULL; ++ g_autofree gchar *value1 = NULL; ++ g_autofree gchar *value2 = NULL; ++ g_autofree gchar *value3 = NULL; ++ g_autofree gchar *value4 = NULL; ++ g_autofree gchar *value5 = NULL; + + scheme1 = gs_utils_get_url_scheme ("appstream://gimp.desktop"); + g_assert_cmpstr (scheme1, ==, "appstream"); +@@ -56,6 +61,17 @@ gs_utils_url_func (void) + g_assert_cmpstr (path2, ==, "gimp.desktop"); + path3 = gs_utils_get_url_path ("apt:/gimp"); + g_assert_cmpstr (path3, ==, "gimp"); ++ ++ value1 = gs_utils_get_url_query_param ("snap://moon-buggy", "channel"); ++ g_assert_null (value1); ++ value2 = gs_utils_get_url_query_param ("snap://moon-buggy?", "channel"); ++ g_assert_null (value2); ++ value3 = gs_utils_get_url_query_param ("snap://moon-buggy?channel=beta", "channel"); ++ g_assert_cmpstr (value3, ==, "beta"); ++ value4 = gs_utils_get_url_query_param ("snap://moon-buggy?channel=beta&foo=bar", "channel"); ++ g_assert_cmpstr (value4, ==, "beta"); ++ value5 = gs_utils_get_url_query_param ("snap://moon-buggy?foo=bar&channel=beta", "channel"); ++ g_assert_cmpstr (value5, ==, "beta"); + } + + static void +diff --git a/lib/gs-utils.c b/lib/gs-utils.c +index cfc556c3..4bd27225 100644 +--- a/lib/gs-utils.c ++++ b/lib/gs-utils.c +@@ -967,6 +967,41 @@ gs_utils_get_url_path (const gchar *url) + return g_strdup (path); + } + ++/** ++ * gs_utils_get_url_query: ++ * @url: A URL, e.g. "snap://moon-buggy?channel=beta" ++ * @url: A parameter name, e.g. "channel" ++ * ++ * Gets a query parameter from the URL string. ++ * ++ * Returns: the URL query parameter, e.g. "beta" ++ */ ++gchar * ++gs_utils_get_url_query_param (const gchar *url, const gchar *name) ++{ ++ g_autoptr(SoupURI) uri = NULL; ++ const gchar *query; ++ g_autofree gchar *prefix = NULL; ++ g_auto(GStrv) params = NULL; ++ int i; ++ ++ uri = soup_uri_new (url); ++ if (!SOUP_URI_IS_VALID (uri)) ++ return NULL; ++ ++ query = soup_uri_get_query (uri); ++ if (query == NULL) ++ return NULL; ++ params = g_strsplit (query, "&", -1); ++ prefix = g_strdup_printf ("%s=", name); ++ for (i = 0; params[i] != NULL; i++) { ++ if (g_str_has_prefix (params[i], prefix)) ++ return g_strdup (params[i] + strlen (prefix)); ++ } ++ ++ return NULL; ++} ++ + /** + * gs_user_agent: + * +diff --git a/lib/gs-utils.h b/lib/gs-utils.h +index 944b9ee9..e5bbd9fc 100644 +--- a/lib/gs-utils.h ++++ b/lib/gs-utils.h +@@ -88,6 +88,8 @@ gboolean gs_utils_is_low_resolution (GtkWidget *toplevel); + + gchar *gs_utils_get_url_scheme (const gchar *url); + gchar *gs_utils_get_url_path (const gchar *url); ++gchar *gs_utils_get_url_query_param (const gchar *url, ++ const gchar *name); + const gchar *gs_user_agent (void); + void gs_utils_append_key_value (GString *str, + gsize align_len, +diff --git a/lib/meson.build b/lib/meson.build +index 47b71a9d..c7455544 100644 +--- a/lib/meson.build ++++ b/lib/meson.build +@@ -42,6 +42,7 @@ install_headers([ + 'gs-app-list.h', + 'gs-auth.h', + 'gs-category.h', ++ 'gs-channel.h', + 'gs-os-release.h', + 'gs-plugin.h', + 'gs-plugin-event.h', +@@ -76,6 +77,7 @@ libgnomesoftware = static_library( + 'gs-app-list.c', + 'gs-auth.c', + 'gs-category.c', ++ 'gs-channel.c', + 'gs-debug.c', + 'gs-os-release.c', + 'gs-plugin.c', +diff --git a/meson.build b/meson.build +index c22a34bb..98c0cf57 100644 +--- a/meson.build ++++ b/meson.build +@@ -166,7 +166,7 @@ if get_option('enable-gudev') + endif + + if get_option('enable-snap') +- snap = dependency('snapd-glib', version : '>= 1.19') ++ snap = dependency('snapd-glib', version : '>= 1.31') + endif + + gnome = import('gnome') +diff --git a/plugins/dummy/gs-plugin-dummy.c b/plugins/dummy/gs-plugin-dummy.c +index 5293fe3b..6d226546 100644 +--- a/plugins/dummy/gs-plugin-dummy.c ++++ b/plugins/dummy/gs-plugin-dummy.c +@@ -835,6 +835,17 @@ gs_plugin_refresh (GsPlugin *plugin, + return gs_plugin_dummy_delay (plugin, app, delay_ms, cancellable, error); + } + ++gboolean ++gs_plugin_app_switch_channel (GsPlugin *plugin, ++ GsApp *app, ++ GsChannel *channel, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_debug ("Switching channel to %s", gs_channel_get_name (channel)); ++ return TRUE; ++} ++ + gboolean + gs_plugin_app_upgrade_download (GsPlugin *plugin, GsApp *app, + GCancellable *cancellable, GError **error) +diff --git a/plugins/snap/gs-plugin-snap.c b/plugins/snap/gs-plugin-snap.c +index cb8cc611..23481911 100644 +--- a/plugins/snap/gs-plugin-snap.c ++++ b/plugins/snap/gs-plugin-snap.c +@@ -37,6 +37,27 @@ struct GsPluginData { + GHashTable *store_snaps; + }; + ++typedef struct { ++ SnapdSnap *snap; ++ gboolean full_details; ++} CacheEntry; ++ ++static CacheEntry * ++cache_entry_new (SnapdSnap *snap, gboolean full_details) ++{ ++ CacheEntry *entry = g_slice_new (CacheEntry); ++ entry->snap = g_object_ref (snap); ++ entry->full_details = full_details; ++ return entry; ++} ++ ++static void ++cache_entry_free (CacheEntry *entry) ++{ ++ g_object_unref (entry->snap); ++ g_slice_free (CacheEntry, entry); ++} ++ + static SnapdClient * + get_client (GsPlugin *plugin, GError **error) + { +@@ -47,10 +68,6 @@ get_client (GsPlugin *plugin, GError **error) + + client = snapd_client_new (); + snapd_client_set_allow_interaction (client, TRUE); +-#ifndef SNAPD_GLIB_VERSION_1_24 +- if (!snapd_client_connect_sync (client, NULL, error)) +- return NULL; +-#endif + old_user_agent = snapd_client_get_user_agent (client); + user_agent = g_strdup_printf ("%s %s", gs_user_agent (), old_user_agent); + snapd_client_set_user_agent (client, user_agent); +@@ -75,7 +92,7 @@ gs_plugin_initialize (GsPlugin *plugin) + } + + priv->store_snaps = g_hash_table_new_full (g_str_hash, g_str_equal, +- g_free, (GDestroyNotify) g_object_unref); ++ g_free, (GDestroyNotify) cache_entry_free); + + priv->auth = gs_auth_new ("snapd"); + gs_auth_set_provider_name (priv->auth, "Snap Store"); +@@ -236,15 +253,24 @@ gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error) + } + + static SnapdSnap * +-store_snap_cache_lookup (GsPlugin *plugin, const gchar *name) ++store_snap_cache_lookup (GsPlugin *plugin, const gchar *name, gboolean need_details) + { + GsPluginData *priv = gs_plugin_get_data (plugin); ++ CacheEntry *entry; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->store_snaps_lock); +- return g_hash_table_lookup (priv->store_snaps, name); ++ ++ entry = g_hash_table_lookup (priv->store_snaps, name); ++ if (entry == NULL) ++ return NULL; ++ ++ if (need_details && !entry->full_details) ++ return NULL; ++ ++ return entry->snap; + } + + static void +-store_snap_cache_update (GsPlugin *plugin, GPtrArray *snaps) ++store_snap_cache_update (GsPlugin *plugin, GPtrArray *snaps, gboolean full_details) + { + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->store_snaps_lock); +@@ -252,7 +278,7 @@ store_snap_cache_update (GsPlugin *plugin, GPtrArray *snaps) + + for (i = 0; i < snaps->len; i++) { + SnapdSnap *snap = snaps->pdata[i]; +- g_hash_table_insert (priv->store_snaps, g_strdup (snapd_snap_get_name (snap)), g_object_ref (snap)); ++ g_hash_table_insert (priv->store_snaps, g_strdup (snapd_snap_get_name (snap)), cache_entry_new (snap, full_details)); + } + } + +@@ -272,7 +298,7 @@ find_snaps (GsPlugin *plugin, SnapdFindFlags flags, const gchar *section, const + return NULL; + } + +- store_snap_cache_update (plugin, snaps); ++ store_snap_cache_update (plugin, snaps, flags & SNAPD_FIND_FLAGS_MATCH_NAME); + + return g_steal_pointer (&snaps); + } +@@ -326,6 +352,8 @@ snap_to_app (GsPlugin *plugin, SnapdSnap *snap) + + if (priv->system_confinement == SNAPD_SYSTEM_CONFINEMENT_STRICT && confinement == SNAPD_CONFINEMENT_STRICT) + gs_app_add_kudo (app, GS_APP_KUDO_SANDBOXED); ++ else ++ gs_app_remove_kudo (app, GS_APP_KUDO_SANDBOXED); + + return g_steal_pointer (&app); + } +@@ -341,6 +369,7 @@ gs_plugin_url_to_app (GsPlugin *plugin, + g_autofree gchar *path = NULL; + g_autoptr(GPtrArray) snaps = NULL; + g_autoptr(GsApp) app = NULL; ++ g_autofree gchar *channel_name = NULL; + + /* not us */ + scheme = gs_utils_get_url_scheme (url); +@@ -354,6 +383,9 @@ gs_plugin_url_to_app (GsPlugin *plugin, + return TRUE; + + app = snap_to_app (plugin, g_ptr_array_index (snaps, 0)); ++ channel_name = gs_utils_get_url_query_param (url, "channel"); ++ if (channel_name != NULL) ++ gs_app_set_metadata (app, "snap::channel", channel_name); + gs_app_list_add (list, app); + + return TRUE; +@@ -610,13 +642,13 @@ gs_plugin_add_search (GsPlugin *plugin, + } + + static SnapdSnap * +-get_store_snap (GsPlugin *plugin, const gchar *name, GCancellable *cancellable, GError **error) ++get_store_snap (GsPlugin *plugin, const gchar *name, gboolean need_details, GCancellable *cancellable, GError **error) + { + SnapdSnap *snap = NULL; + g_autoptr(GPtrArray) snaps = NULL; + + /* use cached version if available */ +- snap = store_snap_cache_lookup (plugin, name); ++ snap = store_snap_cache_lookup (plugin, name, need_details); + if (snap != NULL) + return g_object_ref (snap); + +@@ -775,7 +807,7 @@ load_icon (GsPlugin *plugin, SnapdClient *client, GsApp *app, const gchar *id, S + } + + if (store_snap == NULL) +- store_snap = get_store_snap (plugin, id, cancellable, NULL); ++ store_snap = get_store_snap (plugin, id, FALSE, cancellable, NULL); + if (store_snap != NULL) + return load_store_icon (app, store_snap); + +@@ -791,6 +823,73 @@ gs_plugin_snap_get_description_safe (SnapdSnap *snap) + return g_string_free (str, FALSE); + } + ++static void ++add_channel (GsApp *app, const gchar *name, const gchar *version, const gchar *tracking_channel) ++{ ++ g_autoptr(GsChannel) c = NULL; ++ ++ c = gs_channel_new (name, version); ++ gs_app_add_channel (app, c); ++ if (g_strcmp0 (tracking_channel, name) == 0) ++ gs_app_set_active_channel (app, c); ++} ++ ++static void ++refine_channels (GsApp *app, SnapdSnap *snap, const gchar *tracking_channel) ++{ ++ gchar **tracks; ++ guint i; ++ ++ /* already refined... */ ++ if (gs_app_get_channels (app)->len > 0) ++ return; ++ ++ tracks = snapd_snap_get_tracks (snap); ++ for (i = 0; tracks[i] != NULL; i++) { ++ const gchar *track = tracks[i]; ++ const gchar *risks[] = {"stable", "candidate", "beta", "edge", NULL}; ++ const gchar *last_version = NULL; ++ guint j; ++ ++ last_version = snapd_snap_get_version (snap); ++ for (j = 0; risks[j] != NULL; j++) { ++ const gchar *risk = risks[j]; ++ GPtrArray *channels; ++ g_autofree gchar *name = NULL; ++ const gchar *version = NULL; ++ guint k; ++ ++ channels = snapd_snap_get_channels (snap); ++ ++ if (strcmp (track, "latest") == 0) ++ name = g_strdup (risk); ++ else ++ name = g_strdup_printf ("%s/%s", track, risk); ++ for (k = 0; k < channels->len; k++) { ++ SnapdChannel *channel = channels->pdata[k]; ++ if (strcmp (snapd_channel_get_name (channel), name) == 0) { ++ version = snapd_channel_get_version (channel); ++ break; ++ } ++ } ++ if (version == NULL) ++ version = last_version; ++ add_channel (app, name, version, tracking_channel); ++ ++ /* add any branches for this track/risk */ ++ for (k = 0; k < channels->len; k++) { ++ SnapdChannel *c = channels->pdata[i]; ++ if (snapd_channel_get_branch (c) != NULL && ++ g_strcmp0 (snapd_channel_get_track (c), track) == 0 && ++ g_strcmp0 (snapd_channel_get_risk (c), risk) == 0) ++ add_channel (app, snapd_channel_get_name (c), snapd_channel_get_version (c), tracking_channel); ++ } ++ ++ last_version = version; ++ } ++ } ++} ++ + gboolean + gs_plugin_refine_app (GsPlugin *plugin, + GsApp *app, +@@ -800,7 +899,7 @@ gs_plugin_refine_app (GsPlugin *plugin, + { + GsPluginData *priv = gs_plugin_get_data (plugin); + g_autoptr(SnapdClient) client = NULL; +- const gchar *id, *name; ++ const gchar *id, *name, *tracking_channel = NULL, *store_version = NULL; + g_autoptr(SnapdSnap) local_snap = NULL; + g_autoptr(SnapdSnap) store_snap = NULL; + SnapdSnap *snap; +@@ -822,13 +921,43 @@ gs_plugin_refine_app (GsPlugin *plugin, + + /* get information from local snaps and store */ + local_snap = snapd_client_list_one_sync (client, id, cancellable, NULL); +- if (local_snap == NULL || (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS) != 0) +- store_snap = get_store_snap (plugin, id, cancellable, NULL); ++ /* Need to do full lookup when channel information required ++ * https://forum.snapcraft.io/t/channel-maps-list-is-empty-when-using-v1-snaps-search-as-opposed-to-using-v2-snaps-details */ ++ if (local_snap == NULL || (flags & (GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS | GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANNELS)) != 0) ++ store_snap = get_store_snap (plugin, id, (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANNELS) != 0, cancellable, NULL); + if (local_snap == NULL && store_snap == NULL) + return TRUE; + ++ /* get latest upstream version */ + if (local_snap != NULL) +- gs_app_set_state (app, AS_APP_STATE_INSTALLED); ++ tracking_channel = snapd_snap_get_tracking_channel (local_snap); ++ else ++ tracking_channel = gs_app_get_metadata_item (app, "snap::channel"); ++ if (tracking_channel == NULL) ++ tracking_channel = "stable"; ++ if (store_snap != NULL) { ++ SnapdChannel *c = snapd_snap_match_channel (store_snap, tracking_channel); ++ if (c != NULL) ++ store_version = snapd_channel_get_version (c); ++ else ++ store_version = snapd_snap_get_version (store_snap); ++ if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANNELS) ++ refine_channels (app, store_snap, tracking_channel); ++ } ++ ++ gs_app_set_update_version (app, NULL); ++ if (local_snap != NULL) { ++ if (store_version != NULL && g_strcmp0 (store_version, snapd_snap_get_version (local_snap)) != 0) { ++ gs_app_set_update_version (app, store_version); ++ gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); ++ } ++ else { ++ // Workaround it not being valid to switch from updatable to installed (e.g. if you switch channels) ++ if (gs_app_get_state (app) == AS_APP_STATE_UPDATABLE_LIVE) ++ gs_app_set_state (app, AS_APP_STATE_UNKNOWN); ++ gs_app_set_state (app, AS_APP_STATE_INSTALLED); ++ } ++ } + else + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + +@@ -933,6 +1062,7 @@ gs_plugin_app_install (GsPlugin *plugin, + { + g_autoptr(SnapdClient) client = NULL; + SnapdInstallFlags flags = SNAPD_INSTALL_FLAGS_NONE; ++ const gchar *channel = NULL; + + /* We can only install apps we know of */ + if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0) +@@ -942,10 +1072,39 @@ gs_plugin_app_install (GsPlugin *plugin, + if (client == NULL) + return FALSE; + ++ if (gs_app_get_active_channel (app) != NULL) ++ channel = gs_channel_get_name (gs_app_get_active_channel (app)); ++ + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + if (g_strcmp0 (gs_app_get_metadata_item (app, "snap::confinement"), "classic") == 0) + flags |= SNAPD_INSTALL_FLAGS_CLASSIC; +- if (!snapd_client_install2_sync (client, flags, gs_app_get_id (app), NULL, NULL, progress_cb, app, cancellable, error)) { ++ if (!snapd_client_install2_sync (client, flags, gs_app_get_id (app), channel, NULL, progress_cb, app, cancellable, error)) { ++ gs_app_set_state_recover (app); ++ snapd_error_convert (error); ++ return FALSE; ++ } ++ gs_app_set_state (app, AS_APP_STATE_INSTALLED); ++ return TRUE; ++} ++ ++gboolean ++gs_plugin_update_app (GsPlugin *plugin, ++ GsApp *app, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autoptr(SnapdClient) client = NULL; ++ ++ /* We can only install apps we know of */ ++ if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0) ++ return TRUE; ++ ++ client = get_client (plugin, error); ++ if (client == NULL) ++ return FALSE; ++ ++ gs_app_set_state (app, AS_APP_STATE_INSTALLING); ++ if (!snapd_client_refresh_sync (client, gs_app_get_id (app), NULL, progress_cb, app, cancellable, error)) { + gs_app_set_state_recover (app); + snapd_error_convert (error); + return FALSE; +@@ -1034,6 +1193,31 @@ gs_plugin_launch (GsPlugin *plugin, + return g_app_info_launch (info, NULL, NULL, error); + } + ++gboolean ++gs_plugin_app_switch_channel (GsPlugin *plugin, ++ GsApp *app, ++ GsChannel *channel, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autoptr(SnapdClient) client = NULL; ++ ++ /* We can only modify apps we know of */ ++ if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0) ++ return TRUE; ++ ++ client = get_client (plugin, error); ++ if (client == NULL) ++ return FALSE; ++ ++ if (!snapd_client_switch_sync (client, gs_app_get_id (app), gs_channel_get_name (channel), progress_cb, app, cancellable, error)) { ++ snapd_error_convert (error); ++ return FALSE; ++ } ++ ++ return TRUE; ++} ++ + gboolean + gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, +@@ -1076,21 +1260,15 @@ gs_plugin_auth_login (GsPlugin *plugin, GsAuth *auth, + g_clear_object (&priv->auth_data); + if (priv->snapd_supports_polkit) { + g_autoptr(SnapdClient) client = NULL; +-#ifdef SNAPD_GLIB_VERSION_1_26 + g_autoptr(SnapdUserInformation) user_information = NULL; +-#endif + + client = get_client (plugin, error); + if (client == NULL) + return FALSE; + +-#ifdef SNAPD_GLIB_VERSION_1_26 + user_information = snapd_client_login2_sync (client, gs_auth_get_username (auth), gs_auth_get_password (auth), gs_auth_get_pin (auth), NULL, error); + if (user_information != NULL) + priv->auth_data = g_object_ref (snapd_user_information_get_auth_data (user_information)); +-#else +- priv->auth_data = snapd_client_login_sync (client, gs_auth_get_username (auth), gs_auth_get_password (auth), gs_auth_get_pin (auth), NULL, error); +-#endif + } + else + priv->auth_data = snapd_login_sync (gs_auth_get_username (auth), gs_auth_get_password (auth), gs_auth_get_pin (auth), NULL, error); +diff --git a/src/gs-details-page.c b/src/gs-details-page.c +index 5380541c..42e038e4 100644 +--- a/src/gs-details-page.c ++++ b/src/gs-details-page.c +@@ -110,6 +110,8 @@ struct _GsDetailsPage + GtkWidget *label_details_size_download_value; + GtkWidget *label_details_updated_title; + GtkWidget *label_details_updated_value; ++ GtkWidget *label_details_channel_title; ++ GtkWidget *button_details_channel; + GtkWidget *label_details_version_value; + GtkWidget *label_failed; + GtkWidget *label_license_nonfree_details; +@@ -125,6 +127,7 @@ struct _GsDetailsPage + GtkWidget *spinner_remove; + GtkWidget *stack_details; + GtkWidget *grid_details_kudo; ++ GtkWidget *grid_popover_channel; + GtkWidget *image_details_kudo_docs; + GtkWidget *image_details_kudo_sandboxed; + GtkWidget *image_details_kudo_integration; +@@ -136,6 +139,7 @@ struct _GsDetailsPage + GtkWidget *label_details_kudo_translated; + GtkWidget *label_details_kudo_updated; + GtkWidget *progressbar_top; ++ GtkWidget *popover_channel; + GtkWidget *popover_license_free; + GtkWidget *popover_license_nonfree; + GtkWidget *popover_license_unknown; +@@ -883,6 +887,7 @@ gs_details_page_refresh_all (GsDetailsPage *self) + guint64 kudos; + guint64 updated; + guint64 user_integration_bf; ++ GsChannel *channel; + g_autoptr(GError) error = NULL; + + /* change widgets */ +@@ -959,6 +964,13 @@ gs_details_page_refresh_all (GsDetailsPage *self) + gtk_widget_set_visible (self->button_details_license_unknown, FALSE); + } + ++ /* set channel */ ++ channel = gs_app_get_active_channel (self->app); ++ gtk_widget_set_visible (self->label_details_channel_title, channel != NULL); ++ gtk_widget_set_visible (self->button_details_channel, channel != NULL); ++ if (channel != NULL) ++ gtk_button_set_label (GTK_BUTTON (self->button_details_channel), gs_channel_get_name (channel)); ++ + /* set version */ + tmp = gs_app_get_version (self->app); + if (tmp != NULL){ +@@ -1753,7 +1765,8 @@ gs_details_page_set_local_file (GsDetailsPage *self, GFile *file) + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS | +- GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS, ++ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS | ++ GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANNELS, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, +@@ -1786,6 +1799,7 @@ gs_details_page_set_url (GsDetailsPage *self, const gchar *url) + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS | ++ GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANNELS | + GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, +@@ -1814,7 +1828,8 @@ gs_details_page_load (GsDetailsPage *self) + GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME | + GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS | +- GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS, ++ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS | ++ GS_PLUGIN_REFINE_FLAGS_REQUIRE_CHANNELS, + NULL); + gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, + self->cancellable, +@@ -1930,6 +1945,163 @@ gs_details_page_app_cancel_button_cb (GtkWidget *widget, GsDetailsPage *self) + gs_details_page_remove_app (self); + } + ++typedef struct { ++ GsDetailsPage *self; ++ GsChannel *channel; ++} GsDetailsPageChannelHelper; ++ ++static void ++gs_details_page_channel_helper_free (GsDetailsPageChannelHelper *helper) ++{ ++ g_object_unref (helper->self); ++ g_object_unref (helper->channel); ++ g_free (helper); ++} ++ ++G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsDetailsPageChannelHelper, gs_details_page_channel_helper_free); ++ ++static void ++gs_page_channel_switch_refine_cb (GObject *source, ++ GAsyncResult *res, ++ gpointer user_data) ++{ ++ g_autoptr(GsDetailsPageChannelHelper) helper = (GsDetailsPageChannelHelper *) user_data; ++ GsDetailsPage *self = helper->self; ++ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); ++ gboolean ret; ++ g_autoptr(GError) error = NULL; ++ ++ ret = gs_plugin_loader_job_action_finish (plugin_loader, ++ res, ++ &error); ++ if (g_error_matches (error, ++ GS_PLUGIN_ERROR, ++ GS_PLUGIN_ERROR_CANCELLED)) { ++ g_debug ("%s", error->message); ++ return; ++ } ++ if (!ret) { ++ g_warning ("failed to refine %s: %s", ++ gs_app_get_id (self->app), ++ error->message); ++ return; ++ } ++ ++ gs_details_page_refresh_all (self); ++} ++ ++static void ++gs_page_channel_switched_cb (GObject *source, ++ GAsyncResult *res, ++ gpointer user_data) ++{ ++ g_autoptr(GsDetailsPageChannelHelper) helper = (GsDetailsPageChannelHelper *) user_data; ++ GsDetailsPage *self = helper->self; ++ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); ++ gboolean ret; ++ g_autoptr(GsPluginJob) plugin_job = NULL; ++ g_autoptr(GError) error = NULL; ++ ++ ret = gs_plugin_loader_job_action_finish (plugin_loader, ++ res, ++ &error); ++ if (g_error_matches (error, ++ GS_PLUGIN_ERROR, ++ GS_PLUGIN_ERROR_CANCELLED)) { ++ g_debug ("%s", error->message); ++ return; ++ } ++ if (!ret) { ++ g_warning ("failed to switch channel %s: %s", ++ gs_app_get_id (self->app), ++ error->message); ++ return; ++ } ++ ++ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, ++ "app", self->app, ++ "failure-flags", GS_PLUGIN_FAILURE_FLAGS_NONE, ++ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION, ++ NULL); ++ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, ++ self->app_cancellable, ++ gs_page_channel_switch_refine_cb, ++ g_steal_pointer (&helper)); ++} ++ ++static void ++gs_details_page_switch_channel_cb (GtkWidget *widget, gpointer user_data) ++{ ++ g_autoptr(GsDetailsPageChannelHelper) helper = (GsDetailsPageChannelHelper *) user_data; ++ GsDetailsPage *self = helper->self; ++ g_autoptr(GsPluginJob) plugin_job = NULL; ++ ++ gtk_widget_hide (self->popover_channel); ++ ++ gs_app_set_active_channel (self->app, helper->channel); ++ ++ switch (gs_app_get_state (self->app)) { ++ case AS_APP_STATE_INSTALLED: ++ case AS_APP_STATE_UPDATABLE: ++ case AS_APP_STATE_UPDATABLE_LIVE: ++ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SWITCH_CHANNEL, ++ "app", self->app, ++ "channel", helper->channel, ++ NULL); ++ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job, ++ self->app_cancellable, ++ gs_page_channel_switched_cb, ++ g_steal_pointer (&helper)); ++ break; ++ default: ++ /* not yet installed, just update what we would install */ ++ gs_app_set_version (self->app, gs_channel_get_version (helper->channel)); ++ break; ++ } ++ ++ gs_details_page_refresh_all (self); ++} ++ ++static void ++gs_details_page_channel_cb (GtkWidget *widget, GsDetailsPage *self) ++{ ++ GPtrArray *channels; ++ guint i; ++ ++ gs_container_remove_all (GTK_CONTAINER (self->grid_popover_channel)); ++ channels = gs_app_get_channels (self->app); ++ for (i = 0; i < channels->len; i++) { ++ GsChannel *channel = g_ptr_array_index (channels, i); ++ GtkWidget *label; ++ GtkWidget *button; ++ ++ label = gtk_label_new (gs_channel_get_name (channel)); ++ gtk_label_set_xalign (GTK_LABEL (label), 0.0); ++ gtk_widget_show (label); ++ gtk_grid_attach (GTK_GRID (self->grid_popover_channel), label, 0, i, 1, 1); ++ ++ label = gtk_label_new (gs_channel_get_version (channel)); ++ gtk_label_set_xalign (GTK_LABEL (label), 0.0); ++ gtk_widget_show (label); ++ gtk_grid_attach (GTK_GRID (self->grid_popover_channel), label, 1, i, 1, 1); ++ ++ if (channel != gs_app_get_active_channel (self->app)) { ++ GsDetailsPageChannelHelper *helper = g_new0 (GsDetailsPageChannelHelper, 1); ++ ++ button = gtk_button_new_with_label (_("Switch")); ++ gtk_widget_show (button); ++ gtk_grid_attach (GTK_GRID (self->grid_popover_channel), button, 2, i, 1, 1); ++ helper->self = g_object_ref (self); ++ helper->channel = g_object_ref (channel); ++ g_signal_connect (button, "clicked", ++ G_CALLBACK (gs_details_page_switch_channel_cb), ++ helper); ++ } ++ } ++ ++ gtk_widget_show (self->popover_channel); ++} ++ + static void + gs_details_page_app_install_button_cb (GtkWidget *widget, GsDetailsPage *self) + { +@@ -2398,6 +2570,9 @@ gs_details_page_setup (GsPage *page, + g_signal_connect (self->button_donate, "clicked", + G_CALLBACK (gs_details_page_donate_cb), + self); ++ g_signal_connect (self->button_details_channel, "clicked", ++ G_CALLBACK (gs_details_page_channel_cb), ++ self); + g_signal_connect (self->button_details_license_free, "clicked", + G_CALLBACK (gs_details_page_license_free_cb), + self); +@@ -2495,6 +2670,10 @@ gs_details_page_class_init (GsDetailsPageClass *klass) + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_size_installed_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_updated_title); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_updated_value); ++ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_channel_title); ++ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_details_channel); ++ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, popover_channel); ++ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, grid_popover_channel); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_details_version_value); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_failed); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, list_box_addons); +diff --git a/src/gs-details-page.ui b/src/gs-details-page.ui +index 1943af50..3a6c7b02 100644 +--- a/src/gs-details-page.ui ++++ b/src/gs-details-page.ui +@@ -807,6 +807,51 @@ + False + 9 + 24 ++ ++ ++ True ++ False ++ Channel ++ 0 ++ 0.5 ++ True ++ ++ ++ ++ 0 ++ 0 ++ ++ ++ ++ ++ True ++ False ++ vertical ++ ++ ++ stable ++ True ++ True ++ True ++ start ++ ++ ++ ++ False ++ False ++ 0 ++ ++ ++ ++ ++ 1 ++ 0 ++ ++ + + + True +@@ -821,7 +866,7 @@ + + + 0 +- 0 ++ 1 + + + +@@ -840,7 +885,7 @@ + + + 1 +- 0 ++ 1 + + + +@@ -858,7 +903,7 @@ + + + 0 +- 8 ++ 9 + + + +@@ -878,7 +923,7 @@ + + + 1 +- 8 ++ 9 + + + +@@ -896,7 +941,7 @@ + + + 0 +- 1 ++ 2 + + + +@@ -914,7 +959,7 @@ + + + 1 +- 1 ++ 2 + + + +@@ -931,7 +976,7 @@ + + + 0 +- 2 ++ 3 + + + +@@ -951,7 +996,7 @@ + + + 1 +- 2 ++ 3 + + + +@@ -969,7 +1014,7 @@ + + + 0 +- 6 ++ 7 + + + +@@ -984,7 +1029,7 @@ + + + 1 +- 6 ++ 7 + + + +@@ -1002,7 +1047,7 @@ + + + 0 +- 7 ++ 8 + + + +@@ -1017,7 +1062,7 @@ + + + 1 +- 7 ++ 8 + + + +@@ -1035,7 +1080,7 @@ + + + 0 +- 5 ++ 6 + + + +@@ -1051,7 +1096,7 @@ + + + 1 +- 5 ++ 6 + + + +@@ -1068,7 +1113,7 @@ + + + 0 +- 4 ++ 5 + + + +@@ -1085,7 +1130,7 @@ + + + 1 +- 4 ++ 5 + + + +@@ -1102,7 +1147,7 @@ + + + 0 +- 3 ++ 4 + + + +@@ -1164,7 +1209,7 @@ + + + 1 +- 3 ++ 4 + + + +@@ -1405,6 +1450,19 @@ + + + ++ ++ False ++ 21 ++ button_details_channel ++ ++ ++ True ++ False ++ 9 ++ 12 ++ ++ ++ + + False + 21 +diff --git a/src/gtk-style.css b/src/gtk-style.css +index 0dab2ee2..d43514de 100644 +--- a/src/gtk-style.css ++++ b/src/gtk-style.css +@@ -53,6 +53,37 @@ + border-radius: 16px; + } + ++.details-channel, ++.details-channel:backdrop { ++ outline-offset: 0; ++ background-image: none; ++ border-image: none; ++ border-radius: 4px; ++ border-width: 0 0 2px 0; ++ padding: 1px 9px; ++ box-shadow: none; ++ text-shadow: none; ++ color: #ffffff; ++} ++ ++.details-channel label, ++.details-channel:backdrop label, ++.details-channel:hover label { ++ color: #fff; ++} ++ ++.details-channel { ++ background-color: #4e9a06; ++ border-color: #3e7905; ++} ++.details-channel:hover { ++ background-color: #5db807; ++ border-color: #4d9606; ++} ++.details-channel:backdrop { ++ border-color: #4e9a06; ++} ++ + .details-license-free, + .details-license-nonfree, + .details-license-unknown, +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0012-Don-t-use-colour-to-differentiate-between-free-and-p.patch gnome-software-3.28.1/debian/patches/0012-Don-t-use-colour-to-differentiate-between-free-and-p.patch --- gnome-software-3.27.90/debian/patches/0012-Don-t-use-colour-to-differentiate-between-free-and-p.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0012-Don-t-use-colour-to-differentiate-between-free-and-p.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,38 @@ +From 66f3129d4abc609265e2c2ca860cc8c4fa96bd07 Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Fri, 2 Mar 2018 12:22:11 +1300 +Subject: [PATCH 12/18] Don't use colour to differentiate between free and + proprietary licenses + +--- + src/gtk-style.css | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +diff --git a/src/gtk-style.css b/src/gtk-style.css +index d43514de..8841781a 100644 +--- a/src/gtk-style.css ++++ b/src/gtk-style.css +@@ -141,15 +141,15 @@ + } + + .details-license-nonfree { +- background-color: #ee2222; +- border-color: #c20f0f; ++ background-color: #4e9a06; ++ border-color: #3e7905; + } + .details-license-nonfree:hover { +- background-color: #f25959; +- border-color: #ed1b1b; ++ background-color: #5db807; ++ border-color: #4d9606; + } + .details-license-nonfree:backdrop { +- border-color: #ee2222; ++ border-color: #4e9a06; + } + + .details-license-unknown { +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0013-overview-page-Rotate-featured-apps.patch gnome-software-3.28.1/debian/patches/0013-overview-page-Rotate-featured-apps.patch --- gnome-software-3.27.90/debian/patches/0013-overview-page-Rotate-featured-apps.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0013-overview-page-Rotate-featured-apps.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,357 @@ +From a8085eadb7670d6b5849d861efa66dc1d66b175a Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Wed, 31 Jan 2018 10:20:11 +1300 +Subject: [PATCH 13/18] overview page: Rotate featured apps + +--- + src/gs-overview-page.c | 137 +++++++++++++++++++++++++++++++++++++++++++----- + src/gs-overview-page.ui | 36 +++++++++---- + src/gtk-style.css | 4 ++ + 3 files changed, 155 insertions(+), 22 deletions(-) + +diff --git a/src/gs-overview-page.c b/src/gs-overview-page.c +index 81c8612c..f06abcab 100644 +--- a/src/gs-overview-page.c ++++ b/src/gs-overview-page.c +@@ -35,6 +35,9 @@ + #include "gs-common.h" + + #define N_TILES 9 ++#define FEATURED_ROTATE_TIME 30 /* seconds */ ++#define SWITCHER_ACTIVE_TEXT "●" ++#define SWITCHER_INACTIVE_TEXT "○" + + typedef struct + { +@@ -54,10 +57,12 @@ typedef struct + GHashTable *category_hash; /* id : GsCategory */ + GSettings *settings; + GsApp *third_party_repo; ++ guint featured_rotate_timer_id; + + GtkWidget *infobar_third_party; + GtkWidget *label_third_party; +- GtkWidget *bin_featured; ++ GtkWidget *stack_featured; ++ GtkWidget *box_featured_switcher; + GtkWidget *box_overview; + GtkWidget *box_popular; + GtkWidget *box_popular_rotating; +@@ -366,6 +371,87 @@ out: + gs_overview_page_decrement_action_cnt (self); + } + ++static gboolean ++gs_overview_page_featured_rotate_cb (gpointer user_data) ++{ ++ GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); ++ GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); ++ GtkWidget *visible_child, *next_child = NULL; ++ g_autoptr(GList) banners = NULL; ++ g_autoptr(GList) buttons = NULL; ++ GList *button_link, *banner_link; ++ ++ visible_child = gtk_stack_get_visible_child (GTK_STACK (priv->stack_featured)); ++ banners = gtk_container_get_children (GTK_CONTAINER (priv->stack_featured)); ++ if (banners == NULL) ++ return G_SOURCE_CONTINUE; ++ ++ /* find banner after the currently visible one */ ++ for (banner_link = banners; banner_link != NULL; banner_link = banner_link->next) { ++ GtkWidget *child = banner_link->data; ++ if (child == visible_child) { ++ if (banner_link->next != NULL) ++ next_child = banner_link->next->data; ++ break; ++ } ++ } ++ if (next_child == NULL) ++ next_child = g_list_nth_data (banners, 0); ++ ++ gtk_stack_set_visible_child (GTK_STACK (priv->stack_featured), next_child); ++ ++ /* update switcher */ ++ buttons = gtk_container_get_children (GTK_CONTAINER (priv->box_featured_switcher)); ++ for (banner_link = banners, button_link = buttons; banner_link != NULL && button_link != NULL; banner_link = banner_link->next, button_link = button_link->next) { ++ GtkWidget *event_box = button_link->data, *label; ++ ++ label = gtk_bin_get_child (GTK_BIN (event_box)); ++ if (banner_link->data == next_child) ++ gtk_label_set_label (GTK_LABEL (label), SWITCHER_ACTIVE_TEXT); ++ else ++ gtk_label_set_label (GTK_LABEL (label), SWITCHER_INACTIVE_TEXT); ++ } ++ ++ return G_SOURCE_CONTINUE; ++} ++ ++static void ++reset_rotate_timer (GsOverviewPage *self) ++{ ++ GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); ++ ++ if (priv->featured_rotate_timer_id != 0) ++ g_source_remove (priv->featured_rotate_timer_id); ++ priv->featured_rotate_timer_id = g_timeout_add_seconds (FEATURED_ROTATE_TIME, ++ gs_overview_page_featured_rotate_cb, ++ self); ++} ++ ++static void ++featured_switcher_clicked (GtkWidget *event_box, GdkEventButton *event, gpointer data) ++{ ++ GsOverviewPage *self = GS_OVERVIEW_PAGE (data); ++ GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); ++ g_autoptr(GList) buttons = NULL; ++ g_autoptr(GList) banners = NULL; ++ GList *button_link, *banner_link; ++ ++ buttons = gtk_container_get_children (GTK_CONTAINER (priv->box_featured_switcher)); ++ banners = gtk_container_get_children (GTK_CONTAINER (priv->stack_featured)); ++ for (button_link = buttons, banner_link = banners; button_link != NULL && banner_link != NULL; button_link = button_link->next, banner_link = banner_link->next) { ++ GtkWidget *e = button_link->data, *label; ++ ++ label = gtk_bin_get_child (GTK_BIN (e)); ++ if (e == event_box) { ++ gtk_stack_set_visible_child (GTK_STACK (priv->stack_featured), banner_link->data); ++ reset_rotate_timer (self); ++ gtk_label_set_label (GTK_LABEL (label), SWITCHER_ACTIVE_TEXT); ++ } ++ else ++ gtk_label_set_label (GTK_LABEL (label), SWITCHER_INACTIVE_TEXT); ++ } ++} ++ + static void + gs_overview_page_get_featured_cb (GObject *source_object, + GAsyncResult *res, +@@ -374,8 +460,6 @@ gs_overview_page_get_featured_cb (GObject *source_object, + GsOverviewPage *self = GS_OVERVIEW_PAGE (user_data); + GsOverviewPagePrivate *priv = gs_overview_page_get_instance_private (self); + GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object); +- GtkWidget *tile; +- GsApp *app; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + +@@ -383,8 +467,13 @@ gs_overview_page_get_featured_cb (GObject *source_object, + if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) + goto out; + ++ if (priv->featured_rotate_timer_id != 0) { ++ g_source_remove (priv->featured_rotate_timer_id); ++ priv->featured_rotate_timer_id = 0; ++ } ++ + gtk_widget_hide (priv->featured_heading); +- gs_container_remove_all (GTK_CONTAINER (priv->bin_featured)); ++ gs_container_remove_all (GTK_CONTAINER (priv->stack_featured)); + if (list == NULL) { + g_warning ("failed to get featured apps: %s", + error->message); +@@ -402,16 +491,34 @@ gs_overview_page_get_featured_cb (GObject *source_object, + gs_app_list_randomize (list); + } + +- /* at the moment, we only care about the first app */ +- app = gs_app_list_index (list, 0); +- tile = gs_feature_tile_new (app); +- g_signal_connect (tile, "clicked", +- G_CALLBACK (app_tile_clicked), self); ++ for (guint i = 0; i < gs_app_list_length (list); i++) { ++ GsApp *app = gs_app_list_index (list, i); ++ GtkWidget *tile, *event_box, *label; ++ ++ tile = gs_feature_tile_new (app); ++ g_signal_connect (tile, "clicked", ++ G_CALLBACK (app_tile_clicked), self); ++ ++ gtk_container_add (GTK_CONTAINER (priv->stack_featured), tile); + +- gtk_container_add (GTK_CONTAINER (priv->bin_featured), tile); ++ event_box = gtk_event_box_new (); ++ gtk_widget_show (event_box); ++ g_signal_connect (event_box, "button_release_event", ++ G_CALLBACK (featured_switcher_clicked), self); ++ gtk_box_pack_start (GTK_BOX (priv->box_featured_switcher), event_box, FALSE, TRUE, 0); ++ ++ label = gtk_label_new (i == 0 ? SWITCHER_ACTIVE_TEXT : SWITCHER_INACTIVE_TEXT); ++ gtk_style_context_add_class (gtk_widget_get_style_context (label), ++ "switcher-label"); ++ gtk_widget_show (label); ++ gtk_container_add (GTK_CONTAINER (event_box), label); ++ } + gtk_widget_show (priv->featured_heading); + ++ gtk_widget_set_visible (priv->box_featured_switcher, gs_app_list_length (list) > 1); ++ + priv->empty = FALSE; ++ reset_rotate_timer (self); + + out: + gs_overview_page_decrement_action_cnt (self); +@@ -916,7 +1023,7 @@ gs_overview_page_setup (GsPage *page, + gtk_container_set_focus_vadjustment (GTK_CONTAINER (priv->box_overview), adj); + + tile = gs_feature_tile_new (NULL); +- gtk_container_add (GTK_CONTAINER (priv->bin_featured), tile); ++ gtk_container_add (GTK_CONTAINER (priv->stack_featured), tile); + + for (i = 0; i < N_TILES; i++) { + tile = gs_popular_tile_new (NULL); +@@ -962,6 +1069,11 @@ gs_overview_page_dispose (GObject *object) + g_clear_pointer (&priv->category_of_day, g_free); + g_clear_pointer (&priv->category_hash, g_hash_table_unref); + ++ if (priv->featured_rotate_timer_id != 0) { ++ g_source_remove (priv->featured_rotate_timer_id); ++ priv->featured_rotate_timer_id = 0; ++ } ++ + G_OBJECT_CLASS (gs_overview_page_parent_class)->dispose (object); + } + +@@ -1001,7 +1113,8 @@ gs_overview_page_class_init (GsOverviewPageClass *klass) + + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, infobar_third_party); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, label_third_party); +- gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, bin_featured); ++ gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, stack_featured); ++ gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_featured_switcher); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_overview); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_popular); + gtk_widget_class_bind_template_child_private (widget_class, GsOverviewPage, box_popular_rotating); +diff --git a/src/gs-overview-page.ui b/src/gs-overview-page.ui +index f50a0676..f74be141 100644 +--- a/src/gs-overview-page.ui ++++ b/src/gs-overview-page.ui +@@ -131,9 +131,10 @@ + + + +- ++ + True + fill ++ crossfade + + + False +@@ -141,6 +142,21 @@ + 1 + + ++ ++ ++ center ++ False ++ True ++ horizontal ++ 18 ++ 6 ++ ++ ++ False ++ True ++ 2 ++ ++ + + + +@@ -160,7 +176,7 @@ + + False + True +- 2 ++ 3 + + + +@@ -180,7 +196,7 @@ + + False + True +- 3 ++ 4 + + + +@@ -206,7 +222,7 @@ + + False + True +- 4 ++ 5 + + + +@@ -298,7 +314,7 @@ + + False + True +- 5 ++ 6 + + + +@@ -320,7 +336,7 @@ + + False + True +- 6 ++ 7 + + + +@@ -336,7 +352,7 @@ + + False + True +- 7 ++ 8 + + + +@@ -358,7 +374,7 @@ + + False + True +- 8 ++ 9 + + + +@@ -374,7 +390,7 @@ + + False + True +- 9 ++ 10 + + + +@@ -388,7 +404,7 @@ + + False + True +- 10 ++ 11 + + + +diff --git a/src/gtk-style.css b/src/gtk-style.css +index 8841781a..e6ac110c 100644 +--- a/src/gtk-style.css ++++ b/src/gtk-style.css +@@ -510,3 +510,7 @@ flowboxchild { + background-color: #cfcfcd; + border-bottom: 1px solid darker(#cfcfcd); + } ++ ++.switcher-label { ++ opacity: 0.5; ++} +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0014-Add-a-basic-permissions-system.patch gnome-software-3.28.1/debian/patches/0014-Add-a-basic-permissions-system.patch --- gnome-software-3.27.90/debian/patches/0014-Add-a-basic-permissions-system.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0014-Add-a-basic-permissions-system.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,1983 @@ +From 9c42efa957700f0aa82ee40719b0b50475e9e5ae Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Fri, 26 May 2017 16:30:56 +1200 +Subject: [PATCH 14/20] Add a basic permissions system + +--- + lib/gs-app.c | 55 ++++++++ + lib/gs-app.h | 4 + + lib/gs-permission-value.c | 123 ++++++++++++++++++ + lib/gs-permission-value.h | 47 +++++++ + lib/gs-permission.c | 196 ++++++++++++++++++++++++++++ + lib/gs-permission.h | 57 +++++++++ + lib/gs-plugin-job-private.h | 2 + + lib/gs-plugin-job.c | 61 +++++++++ + lib/gs-plugin-job.h | 6 + + lib/gs-plugin-loader.c | 15 +++ + lib/gs-plugin-types.h | 2 + + lib/gs-plugin-vfuncs.h | 21 +++ + lib/gs-plugin.c | 6 + + lib/meson.build | 4 + + plugins/snap/gs-plugin-snap.c | 213 +++++++++++++++++++++++++++++++ + po/POTFILES.in | 1 + + src/gnome-software.gresource.xml | 1 + + src/gs-details-page.c | 43 +++++++ + src/gs-details-page.ui | 18 ++- + src/gs-page.c | 30 +++++ + src/gs-page.h | 5 + + src/gs-permission-combo-box.c | 125 ++++++++++++++++++ + src/gs-permission-combo-box.h | 41 ++++++ + src/gs-permission-dialog.c | 153 ++++++++++++++++++++++ + src/gs-permission-dialog.h | 39 ++++++ + src/gs-permission-dialog.ui | 67 ++++++++++ + src/gs-permission-switch.c | 100 +++++++++++++++ + src/gs-permission-switch.h | 41 ++++++ + src/meson.build | 3 + + 29 files changed, 1478 insertions(+), 1 deletion(-) + create mode 100644 lib/gs-permission-value.c + create mode 100644 lib/gs-permission-value.h + create mode 100644 lib/gs-permission.c + create mode 100644 lib/gs-permission.h + create mode 100644 src/gs-permission-combo-box.c + create mode 100644 src/gs-permission-combo-box.h + create mode 100644 src/gs-permission-dialog.c + create mode 100644 src/gs-permission-dialog.h + create mode 100644 src/gs-permission-dialog.ui + create mode 100644 src/gs-permission-switch.c + create mode 100644 src/gs-permission-switch.h + +diff --git a/lib/gs-app.c b/lib/gs-app.c +index 02b94228..018e98f4 100644 +--- a/lib/gs-app.c ++++ b/lib/gs-app.c +@@ -90,6 +90,7 @@ typedef struct + gchar *origin; + gchar *origin_appstream; + gchar *origin_hostname; ++ GPtrArray *permissions; + gchar *update_version; + gchar *update_version_ui; + gchar *update_details; +@@ -449,6 +450,18 @@ gs_app_to_string_append (GsApp *app, GString *str) + gs_app_kv_lpad (str, "icon-filename", + as_icon_get_filename (icon)); + } ++ for (i = 0; i < priv->permissions->len; i++) { ++ GsPermission *permission; ++ GsPermissionValue *value; ++ g_autofree gchar *key = NULL; ++ ++ permission = g_ptr_array_index (priv->permissions, i); ++ value = gs_permission_get_value (permission); ++ key = g_strdup_printf ("permission-%02u", i); ++ gs_app_kv_printf (str, key, "[%s] %s", ++ gs_permission_get_label (permission), ++ value ? gs_permission_value_get_label (value) : "(unset)"); ++ } + if (priv->match_value != 0) + gs_app_kv_printf (str, "match-value", "%05x", priv->match_value); + if (priv->priority != 0) +@@ -2607,6 +2620,46 @@ gs_app_set_origin_hostname (GsApp *app, const gchar *origin_hostname) + priv->origin_hostname = g_strdup (origin_hostname); + } + ++/** ++ * gs_app_add_permission: ++ * @app: a #GsApp ++ * @permission: a #GsPermission ++ * ++ * Adds a permission to the applicaton. ++ * ++ * Since: 3.26 ++ **/ ++void ++gs_app_add_permission (GsApp *app, GsPermission *permission) ++{ ++ GsAppPrivate *priv = gs_app_get_instance_private (app); ++ ++ g_return_if_fail (GS_IS_APP (app)); ++ g_return_if_fail (GS_IS_PERMISSION (permission)); ++ ++ g_ptr_array_add (priv->permissions, g_object_ref (permission)); ++} ++ ++/** ++ * gs_app_get_permissions: ++ * @app: a #GsApp ++ * ++ * Gets the list of permissions. ++ * ++ * Returns: (element-type GsPermission) (transfer none): a list ++ * ++ * Since: 3.26 ++ **/ ++GPtrArray * ++gs_app_get_permissions (GsApp *app) ++{ ++ GsAppPrivate *priv = gs_app_get_instance_private (app); ++ ++ g_return_val_if_fail (GS_IS_APP (app), NULL); ++ ++ return priv->permissions; ++} ++ + /** + * gs_app_add_screenshot: + * @app: a #GsApp +@@ -4287,6 +4340,7 @@ gs_app_dispose (GObject *object) + g_clear_pointer (&priv->icons, g_ptr_array_unref); + g_clear_pointer (&priv->channels, g_ptr_array_unref); + g_clear_object (&priv->active_channel); ++ g_clear_pointer (&priv->permissions, g_ptr_array_unref); + + G_OBJECT_CLASS (gs_app_parent_class)->dispose (object); + } +@@ -4476,6 +4530,7 @@ gs_app_init (GsApp *app) + priv->provides = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->icons = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->channels = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); ++ priv->permissions = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->metadata = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, +diff --git a/lib/gs-app.h b/lib/gs-app.h +index 1770909e..25f1f172 100644 +--- a/lib/gs-app.h ++++ b/lib/gs-app.h +@@ -30,6 +30,7 @@ + + #include "gs-channel.h" + #include "gs-price.h" ++#include "gs-permission.h" + + G_BEGIN_DECLS + +@@ -209,6 +210,9 @@ void gs_app_set_origin_appstream (GsApp *app, + const gchar *gs_app_get_origin_hostname (GsApp *app); + void gs_app_set_origin_hostname (GsApp *app, + const gchar *origin_hostname); ++GPtrArray *gs_app_get_permissions (GsApp *app); ++void gs_app_add_permission (GsApp *app, ++ GsPermission *permission); + GPtrArray *gs_app_get_screenshots (GsApp *app); + void gs_app_add_screenshot (GsApp *app, + AsScreenshot *screenshot); +diff --git a/lib/gs-permission-value.c b/lib/gs-permission-value.c +new file mode 100644 +index 00000000..a9143e34 +--- /dev/null ++++ b/lib/gs-permission-value.c +@@ -0,0 +1,123 @@ ++/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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 ++ ++#include "gs-permission-value.h" ++ ++struct _GsPermissionValue ++{ ++ GObject parent_instance; ++ ++ gchar *label; ++ GHashTable *metadata; /* utf8: utf8 */ ++}; ++ ++G_DEFINE_TYPE (GsPermissionValue, gs_permission_value, G_TYPE_OBJECT) ++ ++/** ++ * gs_permission_value_get_metadata_item: ++ * @value: a #GsPermissionValue ++ * @key: a string ++ * ++ * Gets some metadata from a permission value object. ++ * It is left for the the plugin to use this method as required, but a ++ * typical use would be to retrieve an ID for this permission value. ++ * ++ * Returns: A string value, or %NULL for not found ++ */ ++const gchar * ++gs_permission_value_get_metadata_item (GsPermissionValue *value, const gchar *key) ++{ ++ g_return_val_if_fail (GS_IS_PERMISSION_VALUE (value), NULL); ++ g_return_val_if_fail (key != NULL, NULL); ++ return g_hash_table_lookup (value->metadata, key); ++} ++ ++/** ++ * gs_permission_value_add_metadata: ++ * @value: a #GsPermissionValue ++ * @key: a string ++ * @value: a string ++ * ++ * Adds metadata to the permission object. ++ * It is left for the the plugin to use this method as required, but a ++ * typical use would be to store an ID for this permission. ++ */ ++void ++gs_permission_value_add_metadata (GsPermissionValue *value, const gchar *key, const gchar *val) ++{ ++ g_return_if_fail (GS_IS_PERMISSION_VALUE (value)); ++ g_hash_table_insert (value->metadata, g_strdup (key), g_strdup (val)); ++} ++ ++/** ++ * gs_permission_value_get_label: ++ * @permission: a #GsPermissionValue ++ * ++ * Get the label for this permission. ++ * ++ * Returns: a label string. ++ */ ++const gchar * ++gs_permission_value_get_label (GsPermissionValue *value) ++{ ++ g_return_val_if_fail (GS_IS_PERMISSION_VALUE (value), NULL); ++ return value->label; ++} ++ ++static void ++gs_permission_value_finalize (GObject *object) ++{ ++ GsPermissionValue *value = GS_PERMISSION_VALUE (object); ++ ++ g_free (value->label); ++ g_hash_table_unref (value->metadata); ++ ++ G_OBJECT_CLASS (gs_permission_value_parent_class)->finalize (object); ++} ++ ++static void ++gs_permission_value_class_init (GsPermissionValueClass *klass) ++{ ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ object_class->finalize = gs_permission_value_finalize; ++} ++ ++static void ++gs_permission_value_init (GsPermissionValue *value) ++{ ++ value->metadata = g_hash_table_new_full (g_str_hash, g_str_equal, ++ g_free, g_free); ++} ++ ++GsPermissionValue * ++gs_permission_value_new (const gchar *label) ++{ ++ GsPermissionValue *value; ++ value = g_object_new (GS_TYPE_PERMISSION_VALUE, NULL); ++ value->label = g_strdup (label); ++ return GS_PERMISSION_VALUE (value); ++} ++ ++/* vim: set noexpandtab: */ +diff --git a/lib/gs-permission-value.h b/lib/gs-permission-value.h +new file mode 100644 +index 00000000..b2ee3b40 +--- /dev/null ++++ b/lib/gs-permission-value.h +@@ -0,0 +1,47 @@ ++/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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_PERMISSION_VALUE_H ++#define __GS_PERMISSION_VALUE_H ++ ++#include ++ ++G_BEGIN_DECLS ++ ++#define GS_TYPE_PERMISSION_VALUE (gs_permission_value_get_type ()) ++ ++G_DECLARE_FINAL_TYPE (GsPermissionValue, gs_permission_value, GS, PERMISSION_VALUE, GObject) ++ ++GsPermissionValue *gs_permission_value_new (const gchar *label); ++ ++const gchar *gs_permission_value_get_metadata_item (GsPermissionValue *value, ++ const gchar *key); ++void gs_permission_value_add_metadata (GsPermissionValue *value, ++ const gchar *key, ++ const gchar *val); ++ ++const gchar *gs_permission_value_get_label (GsPermissionValue *value); ++ ++G_END_DECLS ++ ++#endif /* __GS_PERMISSION_VALUE_H */ ++ ++/* vim: set noexpandtab: */ +diff --git a/lib/gs-permission.c b/lib/gs-permission.c +new file mode 100644 +index 00000000..4ab5e370 +--- /dev/null ++++ b/lib/gs-permission.c +@@ -0,0 +1,196 @@ ++/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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 ++ ++#include "gs-permission.h" ++ ++struct _GsPermission ++{ ++ GObject parent_instance; ++ ++ gchar *label; ++ GPtrArray *values; ++ GsPermissionValue *value; ++ GHashTable *metadata; /* utf8: utf8 */ ++}; ++ ++G_DEFINE_TYPE (GsPermission, gs_permission, G_TYPE_OBJECT) ++ ++/** ++ * gs_permission_get_metadata_item: ++ * @permission: a #GsPermission ++ * @key: a string ++ * ++ * Gets some metadata from a permission object. ++ * It is left for the the plugin to use this method as required, but a ++ * typical use would be to retrieve an ID for this permission. ++ * ++ * Returns: A string value, or %NULL for not found ++ */ ++const gchar * ++gs_permission_get_metadata_item (GsPermission *permission, const gchar *key) ++{ ++ g_return_val_if_fail (GS_IS_PERMISSION (permission), NULL); ++ g_return_val_if_fail (key != NULL, NULL); ++ return g_hash_table_lookup (permission->metadata, key); ++} ++ ++/** ++ * gs_permission_add_metadata: ++ * @permission: a #GsPermission ++ * @key: a string ++ * @value: a string ++ * ++ * Adds metadata to the permission object. ++ * It is left for the the plugin to use this method as required, but a ++ * typical use would be to store an ID for this permission. ++ */ ++void ++gs_permission_add_metadata (GsPermission *permission, const gchar *key, const gchar *value) ++{ ++ g_return_if_fail (GS_IS_PERMISSION (permission)); ++ g_hash_table_insert (permission->metadata, g_strdup (key), g_strdup (value)); ++} ++ ++/** ++ * gs_permission_get_label: ++ * @permission: a #GsPermission ++ * ++ * Get the label for this permission. ++ * ++ * Returns: a label string. ++ */ ++const gchar * ++gs_permission_get_label (GsPermission *permission) ++{ ++ g_return_val_if_fail (GS_IS_PERMISSION (permission), NULL); ++ return permission->label; ++} ++ ++/** ++ * gs_permission_add_value: ++ * @permission: a #GsPermission ++ * @value: a #GsPermissionValue ++ * ++ * Add a possible values for this permission. ++ */ ++void ++gs_permission_add_value (GsPermission *permission, GsPermissionValue *value) ++{ ++ g_return_if_fail (GS_IS_PERMISSION (permission)); ++ g_ptr_array_add (permission->values, g_object_ref (value)); ++} ++ ++/** ++ * gs_permission_get_values: ++ * @permission: a #GsPermission ++ * ++ * Get the possible values for this permission. ++ * ++ * Returns: (element-type GsPermissionValue) (transfer none): a list ++ */ ++GPtrArray * ++gs_permission_get_values (GsPermission *permission) ++{ ++ g_return_val_if_fail (GS_IS_PERMISSION (permission), NULL); ++ return permission->values; ++} ++ ++/** ++ * gs_permission_get_value: ++ * @permission: a #GsPermission ++ * ++ * Get the value for this permission. ++ * ++ * Returns: a %GsPermissionValue or %NULL. ++ */ ++GsPermissionValue * ++gs_permission_get_value (GsPermission *permission) ++{ ++ g_return_val_if_fail (GS_IS_PERMISSION (permission), NULL); ++ return permission->value; ++} ++ ++/** ++ * gs_permission_set_value: ++ * @permission: a #GsPermission ++ * @value: a #GsPermissionValue to set for this permission ++ * ++ * Set the value of this permission. ++ */ ++void ++gs_permission_set_value (GsPermission *permission, GsPermissionValue *value) ++{ ++ g_return_if_fail (GS_IS_PERMISSION (permission)); ++ g_set_object (&permission->value, value); ++} ++ ++static void ++gs_permission_dispose (GObject *object) ++{ ++ GsPermission *permission = GS_PERMISSION (object); ++ ++ g_clear_pointer (&permission->values, g_ptr_array_unref); ++ g_clear_object (&permission->value); ++ ++ G_OBJECT_CLASS (gs_permission_parent_class)->dispose (object); ++} ++ ++static void ++gs_permission_finalize (GObject *object) ++{ ++ GsPermission *permission = GS_PERMISSION (object); ++ ++ g_free (permission->label); ++ g_hash_table_unref (permission->metadata); ++ ++ G_OBJECT_CLASS (gs_permission_parent_class)->finalize (object); ++} ++ ++static void ++gs_permission_class_init (GsPermissionClass *klass) ++{ ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ object_class->dispose = gs_permission_dispose; ++ object_class->finalize = gs_permission_finalize; ++} ++ ++static void ++gs_permission_init (GsPermission *permission) ++{ ++ permission->metadata = g_hash_table_new_full (g_str_hash, g_str_equal, ++ g_free, g_free); ++ permission->values = g_ptr_array_new_with_free_func (g_object_unref); ++} ++ ++GsPermission * ++gs_permission_new (const gchar *label) ++{ ++ GsPermission *permission; ++ permission = g_object_new (GS_TYPE_PERMISSION, NULL); ++ permission->label = g_strdup (label); ++ return GS_PERMISSION (permission); ++} ++ ++/* vim: set noexpandtab: */ +diff --git a/lib/gs-permission.h b/lib/gs-permission.h +new file mode 100644 +index 00000000..08a24366 +--- /dev/null ++++ b/lib/gs-permission.h +@@ -0,0 +1,57 @@ ++/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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_PERMISSION_H ++#define __GS_PERMISSION_H ++ ++#include ++ ++#include "gs-permission-value.h" ++ ++G_BEGIN_DECLS ++ ++#define GS_TYPE_PERMISSION (gs_permission_get_type ()) ++ ++G_DECLARE_FINAL_TYPE (GsPermission, gs_permission, GS, PERMISSION, GObject) ++ ++GsPermission *gs_permission_new (const gchar *label); ++ ++const gchar *gs_permission_get_metadata_item (GsPermission *permission, ++ const gchar *key); ++void gs_permission_add_metadata (GsPermission *permission, ++ const gchar *key, ++ const gchar *value); ++ ++const gchar *gs_permission_get_label (GsPermission *permission); ++ ++void gs_permission_add_value (GsPermission *permission, ++ GsPermissionValue *value); ++GPtrArray *gs_permission_get_values (GsPermission *permission); ++ ++GsPermissionValue *gs_permission_get_value (GsPermission *permission); ++void gs_permission_set_value (GsPermission *permission, ++ GsPermissionValue *value); ++ ++G_END_DECLS ++ ++#endif /* __GS_PERMISSION_H */ ++ ++/* vim: set noexpandtab: */ +diff --git a/lib/gs-plugin-job-private.h b/lib/gs-plugin-job-private.h +index 414414da..bd8781f4 100644 +--- a/lib/gs-plugin-job-private.h ++++ b/lib/gs-plugin-job-private.h +@@ -54,6 +54,8 @@ GsCategory *gs_plugin_job_get_category (GsPluginJob *self); + AsReview *gs_plugin_job_get_review (GsPluginJob *self); + GsPrice *gs_plugin_job_get_price (GsPluginJob *self); + GsChannel *gs_plugin_job_get_channel (GsPluginJob *self); ++GsPermission *gs_plugin_job_get_permission (GsPluginJob *self); ++GsPermissionValue *gs_plugin_job_get_permission_value (GsPluginJob *self); + gchar *gs_plugin_job_to_string (GsPluginJob *self); + void gs_plugin_job_set_action (GsPluginJob *self, + GsPluginAction action); +diff --git a/lib/gs-plugin-job.c b/lib/gs-plugin-job.c +index cf9ab6ca..7acc891e 100644 +--- a/lib/gs-plugin-job.c ++++ b/lib/gs-plugin-job.c +@@ -50,6 +50,8 @@ struct _GsPluginJob + AsReview *review; + GsPrice *price; + GsChannel *channel; ++ GsPermission *permission; ++ GsPermissionValue *permission_value; + gint64 time_created; + }; + +@@ -72,6 +74,8 @@ enum { + PROP_PRICE, + PROP_CHANNEL, + PROP_TIMEOUT, ++ PROP_PERMISSION, ++ PROP_PERMISSION_VALUE, + PROP_LAST + }; + +@@ -142,6 +146,12 @@ gs_plugin_job_to_string (GsPluginJob *self) + g_autofree gchar *path = g_file_get_path (self->file); + g_string_append_printf (str, " with file=%s", path); + } ++ if (self->permission != NULL) { ++ g_string_append_printf (str, " with permission=%s", gs_permission_get_label (self->permission)); ++ } ++ if (self->permission_value != NULL) { ++ g_string_append_printf (str, " with permission-value=%s", gs_permission_value_get_label (self->permission_value)); ++ } + if (self->plugin != NULL) { + g_string_append_printf (str, " on plugin=%s", + gs_plugin_get_name (self->plugin)); +@@ -472,6 +482,34 @@ gs_plugin_job_get_channel (GsPluginJob *self) + return self->channel; + } + ++void ++gs_plugin_job_set_permission (GsPluginJob *self, GsPermission *permission) ++{ ++ g_return_if_fail (GS_IS_PLUGIN_JOB (self)); ++ g_set_object (&self->permission, permission); ++} ++ ++GsPermission * ++gs_plugin_job_get_permission (GsPluginJob *self) ++{ ++ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); ++ return self->permission; ++} ++ ++void ++gs_plugin_job_set_permission_value (GsPluginJob *self, GsPermissionValue *value) ++{ ++ g_return_if_fail (GS_IS_PLUGIN_JOB (self)); ++ g_set_object (&self->permission_value, value); ++} ++ ++GsPermissionValue * ++gs_plugin_job_get_permission_value (GsPluginJob *self) ++{ ++ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); ++ return self->permission_value; ++} ++ + static void + gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) + { +@@ -529,6 +567,12 @@ gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSp + case PROP_TIMEOUT: + g_value_set_uint (value, self->timeout); + break; ++ case PROP_PERMISSION: ++ g_value_set_object (value, self->permission); ++ break; ++ case PROP_PERMISSION_VALUE: ++ g_value_set_object (value, self->permission_value); ++ break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; +@@ -592,6 +636,12 @@ gs_plugin_job_set_property (GObject *obj, guint prop_id, const GValue *value, GP + case PROP_CHANNEL: + gs_plugin_job_set_channel (self, g_value_get_object (value)); + break; ++ case PROP_PERMISSION: ++ gs_plugin_job_set_permission (self, g_value_get_object (value)); ++ break; ++ case PROP_PERMISSION_VALUE: ++ gs_plugin_job_set_permission_value (self, g_value_get_object (value)); ++ break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; +@@ -612,6 +662,7 @@ gs_plugin_job_finalize (GObject *obj) + g_clear_object (&self->review); + g_clear_object (&self->price); + g_clear_object (&self->channel); ++ g_clear_object (&self->permission); + G_OBJECT_CLASS (gs_plugin_job_parent_class)->finalize (obj); + } + +@@ -710,6 +761,16 @@ gs_plugin_job_class_init (GsPluginJobClass *klass) + GS_TYPE_CHANNEL, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_CHANNEL, pspec); ++ ++ pspec = g_param_spec_object ("permission", NULL, NULL, ++ GS_TYPE_PERMISSION, ++ G_PARAM_READWRITE); ++ g_object_class_install_property (object_class, PROP_PERMISSION, pspec); ++ ++ pspec = g_param_spec_object ("permission-value", NULL, NULL, ++ GS_TYPE_PERMISSION_VALUE, ++ G_PARAM_READWRITE); ++ g_object_class_install_property (object_class, PROP_PERMISSION_VALUE, pspec); + } + + static void +diff --git a/lib/gs-plugin-job.h b/lib/gs-plugin-job.h +index 48c3fa39..1bdea8d5 100644 +--- a/lib/gs-plugin-job.h ++++ b/lib/gs-plugin-job.h +@@ -29,6 +29,8 @@ + #include "gs-category.h" + #include "gs-plugin-types.h" + #include "gs-price.h" ++#include "gs-permission.h" ++#include "gs-permission-value.h" + + G_BEGIN_DECLS + +@@ -74,6 +76,10 @@ void gs_plugin_job_set_price (GsPluginJob *self, + GsPrice *price); + void gs_plugin_job_set_channel (GsPluginJob *self, + GsChannel *channel); ++void gs_plugin_job_set_permission (GsPluginJob *self, ++ GsPermission *permission); ++void gs_plugin_job_set_permission_value (GsPluginJob *self, ++ GsPermissionValue *value); + + #define gs_plugin_job_newv(a,...) GS_PLUGIN_JOB(g_object_new(GS_TYPE_PLUGIN_JOB, "action", a, __VA_ARGS__)) + +diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c +index af69ea2c..47a86e32 100644 +--- a/lib/gs-plugin-loader.c ++++ b/lib/gs-plugin-loader.c +@@ -138,6 +138,12 @@ typedef gboolean (*GsPluginSwitchChannelFunc) (GsPlugin *plugin, + GsChannel *channel, + GCancellable *cancellable, + GError **error); ++typedef gboolean (*GsPluginSetPermissionFunc) (GsPlugin *plugin, ++ GsApp *app, ++ GsPermission *permission, ++ GsPermissionValue *value, ++ GCancellable *cancellable, ++ GError **error); + typedef gboolean (*GsPluginReviewFunc) (GsPlugin *plugin, + GsApp *app, + AsReview *review, +@@ -650,6 +656,15 @@ gs_plugin_loader_call_vfunc (GsPluginLoaderHelper *helper, + cancellable, &error_local); + } + break; ++ case GS_PLUGIN_ACTION_SET_PERMISSION: ++ { ++ GsPluginSetPermissionFunc plugin_func = func; ++ ret = plugin_func (plugin, app, ++ gs_plugin_job_get_permission (helper->plugin_job), ++ gs_plugin_job_get_permission_value (helper->plugin_job), ++ cancellable, &error_local); ++ } ++ break; + case GS_PLUGIN_ACTION_REVIEW_SUBMIT: + case GS_PLUGIN_ACTION_REVIEW_UPVOTE: + case GS_PLUGIN_ACTION_REVIEW_DOWNVOTE: +diff --git a/lib/gs-plugin-types.h b/lib/gs-plugin-types.h +index 38f13290..bc387b1d 100644 +--- a/lib/gs-plugin-types.h ++++ b/lib/gs-plugin-types.h +@@ -276,6 +276,7 @@ typedef enum { + * @GS_PLUGIN_ACTION_DESTROY: Destroy the plugin + * @GS_PLUGIN_ACTION_PURCHASE: Purchase an app + * @GS_PLUGIN_ACTION_SWITCH_CHANNEL: Switch app channel ++ * @GS_PLUGIN_ACTION_SET_PERMISSION: Set app permission + * + * The plugin action. + **/ +@@ -324,6 +325,7 @@ typedef enum { + GS_PLUGIN_ACTION_DESTROY, + GS_PLUGIN_ACTION_PURCHASE, + GS_PLUGIN_ACTION_SWITCH_CHANNEL, ++ GS_PLUGIN_ACTION_SET_PERMISSION, + /*< private >*/ + GS_PLUGIN_ACTION_LAST + } GsPluginAction; +diff --git a/lib/gs-plugin-vfuncs.h b/lib/gs-plugin-vfuncs.h +index 61447150..0aa20641 100644 +--- a/lib/gs-plugin-vfuncs.h ++++ b/lib/gs-plugin-vfuncs.h +@@ -41,6 +41,7 @@ + #include "gs-app-list.h" + #include "gs-category.h" + #include "gs-price.h" ++#include "gs-permission.h" + + G_BEGIN_DECLS + +@@ -745,6 +746,26 @@ gboolean gs_plugin_app_upgrade_trigger (GsPlugin *plugin, + GCancellable *cancellable, + GError **error); + ++/** ++ * gs_plugin_app_set_permission: ++ * @plugin: a #GsPlugin ++ * @app: a #GsApp ++ * @permission: a #GsPermission to set ++ * @value: value to set for the permission ++ * @cancellable: a #GCancellable, or %NULL ++ * @error: a #GError, or %NULL ++ * ++ * Set an app permission. ++ * ++ * Returns: %TRUE for success or if not relevant ++ **/ ++gboolean gs_plugin_app_set_permission (GsPlugin *plugin, ++ GsApp *app, ++ GsPermission *permission, ++ GsPermissionValue *value, ++ GCancellable *cancellable, ++ GError **error); ++ + /** + * gs_plugin_review_submit: + * @plugin: a #GsPlugin +diff --git a/lib/gs-plugin.c b/lib/gs-plugin.c +index f6e7a3f1..b6a6854b 100644 +--- a/lib/gs-plugin.c ++++ b/lib/gs-plugin.c +@@ -1771,6 +1771,8 @@ gs_plugin_action_to_function_name (GsPluginAction action) + return "gs_plugin_app_purchase"; + if (action == GS_PLUGIN_ACTION_SWITCH_CHANNEL) + return "gs_plugin_app_switch_channel"; ++ if (action == GS_PLUGIN_ACTION_SET_PERMISSION) ++ return "gs_plugin_app_set_permission"; + return NULL; + } + +@@ -1873,6 +1875,8 @@ gs_plugin_action_to_string (GsPluginAction action) + return "purchase"; + if (action == GS_PLUGIN_ACTION_SWITCH_CHANNEL) + return "switch-channel"; ++ if (action == GS_PLUGIN_ACTION_SET_PERMISSION) ++ return "set-permission"; + return NULL; + } + +@@ -1975,6 +1979,8 @@ gs_plugin_action_from_string (const gchar *action) + return GS_PLUGIN_ACTION_PURCHASE; + if (g_strcmp0 (action, "switch-channel") == 0) + return GS_PLUGIN_ACTION_SWITCH_CHANNEL; ++ if (g_strcmp0 (action, "set-permission") == 0) ++ return GS_PLUGIN_ACTION_SET_PERMISSION; + return GS_PLUGIN_ACTION_UNKNOWN; + } + +diff --git a/lib/meson.build b/lib/meson.build +index c7455544..699dcacc 100644 +--- a/lib/meson.build ++++ b/lib/meson.build +@@ -44,6 +44,8 @@ install_headers([ + 'gs-category.h', + 'gs-channel.h', + 'gs-os-release.h', ++ 'gs-permission.h', ++ 'gs-permission-value.h', + 'gs-plugin.h', + 'gs-plugin-event.h', + 'gs-plugin-types.h', +@@ -80,6 +82,8 @@ libgnomesoftware = static_library( + 'gs-channel.c', + 'gs-debug.c', + 'gs-os-release.c', ++ 'gs-permission.c', ++ 'gs-permission-value.c', + 'gs-plugin.c', + 'gs-plugin-event.c', + 'gs-plugin-job.c', +diff --git a/plugins/snap/gs-plugin-snap.c b/plugins/snap/gs-plugin-snap.c +index fbd9b2fb..50f20590 100644 +--- a/plugins/snap/gs-plugin-snap.c ++++ b/plugins/snap/gs-plugin-snap.c +@@ -1029,6 +1029,170 @@ gs_plugin_refine_app (GsPlugin *plugin, + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON && gs_app_get_pixbuf (app) == NULL) + load_icon (plugin, client, app, id, local_snap, store_snap, cancellable); + ++ if (gs_app_get_permissions (app)->len == 0) { ++ g_autoptr(GPtrArray) plugs = NULL; ++ g_autoptr(GPtrArray) slots = NULL; ++ guint i; ++ ++ if (!snapd_client_get_interfaces_sync (client, &plugs, &slots, cancellable, error)) ++ return FALSE; ++ for (i = 0; i < plugs->len; i++) { ++ SnapdPlug *plug = plugs->pdata[i]; ++ const gchar *interface_name, *label; ++ g_autoptr(GsPermission) permission = NULL; ++ SnapdConnection *connection = NULL; ++ guint j; ++ ++ /* skip if not relating to this snap */ ++ if (g_strcmp0 (snapd_plug_get_snap (plug), id) != 0) ++ continue; ++ ++ interface_name = snapd_plug_get_interface (plug); ++ if (strcmp (interface_name, "account-control") == 0) { ++ label = _("Add user accounts and change passwords"); ++ } else if (strcmp (interface_name, "alsa") == 0) { ++ label = _("Play and record sound"); ++ } else if (strcmp (interface_name, "avahi-observe") == 0) { ++ label = _("Detect network devices using mDNS/DNS-SD (Bonjour/zeroconf)"); ++ } else if (strcmp (interface_name, "bluetooth-control") == 0) { ++ label = _("Access bluetooth hardware directly"); ++ } else if (strcmp (interface_name, "bluez") == 0) { ++ label = _("Use bluetooth devices"); ++ } else if (strcmp (interface_name, "camera") == 0) { ++ label = _("Use your camera"); ++ } else if (strcmp (interface_name, "cups-control") == 0) { ++ label = _("Print documents"); ++ } else if (strcmp (interface_name, "joystick") == 0) { ++ label = _("Use any connected joystick"); ++ } else if (strcmp (interface_name, "docker") == 0) { ++ label = _("Allow connecting to the Docker service"); ++ } else if (strcmp (interface_name, "firewall-control") == 0) { ++ label = _("Configure network firewall"); ++ } else if (strcmp (interface_name, "fuse-support") == 0) { ++ label = _("Setup and use privileged FUSE filesystems"); ++ } else if (strcmp (interface_name, "fwupd") == 0) { ++ label = _("Update firmware on this device"); ++ } else if (strcmp (interface_name, "hardware-observe") == 0) { ++ label = _("Access hardware information"); ++ } else if (strcmp (interface_name, "hardware-random-control") == 0) { ++ label = _("Provide entropy to hardware random number generator"); ++ } else if (strcmp (interface_name, "hardware-random-observe") == 0) { ++ label = _("Use hardware-generated random numbers"); ++ } else if (strcmp (interface_name, "home") == 0) { ++ label = _("Access files in your home folder"); ++ } else if (strcmp (interface_name, "libvirt") == 0) { ++ label = _("Access libvirt service"); ++ } else if (strcmp (interface_name, "locale-control") == 0) { ++ label = _("Change system language and region settings"); ++ } else if (strcmp (interface_name, "location-control") == 0) { ++ label = _("Change location settings and providers"); ++ } else if (strcmp (interface_name, "location-observe") == 0) { ++ label = _("Access your location"); ++ } else if (strcmp (interface_name, "log-observe") == 0) { ++ label = _("Read system and application logs"); ++ } else if (strcmp (interface_name, "lxd") == 0) { ++ label = _("Access LXD service"); ++ //} else if (strcmp (interface_name, "media-hub") == 0) { ++ // label = _("access the media-hub service"); ++ } else if (strcmp (interface_name, "modem-manager") == 0) { ++ label = _("Use and configure modems"); ++ } else if (strcmp (interface_name, "mount-observe") == 0) { ++ label = _("Read system mount information and disk quotas"); ++ } else if (strcmp (interface_name, "mpris") == 0) { ++ label = _("Control music and video players"); ++ } else if (strcmp (interface_name, "network-control") == 0) { ++ label = _("Change low-level network settings"); ++ } else if (strcmp (interface_name, "network-manager") == 0) { ++ label = _("Access the NetworkManager service to read and change network settings"); ++ } else if (strcmp (interface_name, "network-observe") == 0) { ++ label = _("Read access to network settings"); ++ } else if (strcmp (interface_name, "network-setup-control") == 0) { ++ label = _("Change network settings"); ++ } else if (strcmp (interface_name, "network-setup-observe") == 0) { ++ label = _("Read network settings"); ++ } else if (strcmp (interface_name, "ofono") == 0) { ++ label = _("Access the ofono service to read and change network settings for mobile telephony"); ++ } else if (strcmp (interface_name, "openvtswitch") == 0) { ++ label = _("Control Open vSwitch hardware"); ++ } else if (strcmp (interface_name, "optical-drive") == 0) { ++ label = _("Read from CD/DVD"); ++ } else if (strcmp (interface_name, "password-manager-service") == 0) { ++ label = _("Read, add, change, or remove saved passwords"); ++ } else if (strcmp (interface_name, "ppp") == 0) { ++ label = _("Access pppd and ppp devices for configuring Point-to-Point Protocol connections"); ++ } else if (strcmp (interface_name, "process-control") == 0) { ++ label = _("Pause or end any process on the system"); ++ } else if (strcmp (interface_name, "pulseaudio") == 0) { ++ label = _("Play and record sound"); ++ } else if (strcmp (interface_name, "raw-usb") == 0) { ++ label = _("Access USB hardware directly"); ++ } else if (strcmp (interface_name, "removable-media") == 0) { ++ label = _("Read/write files on removable storage devices"); ++ } else if (strcmp (interface_name, "screen-inhibit-control") == 0) { ++ label = _("Prevent screen sleep/lock"); ++ } else if (strcmp (interface_name, "serial-port") == 0) { ++ label = _("Access serial port hardware"); ++ } else if (strcmp (interface_name, "shutdown") == 0) { ++ label = _("Restart or power off the device"); ++ } else if (strcmp (interface_name, "snapd-control") == 0) { ++ label = _("Install, remove and configure software"); ++ } else if (strcmp (interface_name, "storage-framework-service") == 0) { ++ label = _("Access Storage Framework service"); ++ } else if (strcmp (interface_name, "system-observe") == 0) { ++ label = _("Read process and system information"); ++ } else if (strcmp (interface_name, "system-trace") == 0) { ++ label = _("Monitor and control any running program"); ++ } else if (strcmp (interface_name, "time-control") == 0) { ++ label = _("Change the date and time"); ++ } else if (strcmp (interface_name, "timeserver-control") == 0) { ++ label = _("Change time server settings"); ++ } else if (strcmp (interface_name, "timezone-control") == 0) { ++ label = _("Change the time zone"); ++ } else if (strcmp (interface_name, "udisks2") == 0) { ++ label = _("Access the UDisks2 service for configuring disks and removable media"); ++ } else if (strcmp (interface_name, "unity8-calendar") == 0) { ++ label = _("Read/change shared calendar events in Ubuntu Unity 8"); ++ } else if (strcmp (interface_name, "unity8-contacts") == 0) { ++ label = _("Read/change shared contacts in Ubuntu Unity 8"); ++ } else if (strcmp (interface_name, "upower-observe") == 0) { ++ label = _("Access energy usage data"); ++ } else { ++ g_debug ("Skipping plug with interface %s", interface_name); ++ continue; ++ } ++ /* map interfaces to known permissions */ ++ permission = gs_permission_new (label); ++ gs_permission_add_metadata (permission, "snap::plug", snapd_plug_get_name (plug)); ++ ++ if (snapd_plug_get_connections (plug)->len > 0) ++ connection = g_ptr_array_index (snapd_plug_get_connections (plug), 0); ++ for (j = 0; j < slots->len; j++) { ++ SnapdSlot *slot = slots->pdata[j]; ++ g_autoptr(GsPermissionValue) value = NULL; ++ g_autofree gchar *value_label = NULL; ++ ++ /* skip slots we can't connect to */ ++ if (g_strcmp0 (snapd_plug_get_interface (plug), snapd_slot_get_interface (slot)) != 0) ++ continue; ++ ++ if (strcmp (snapd_slot_get_snap (slot), "core") == 0) ++ value_label = g_strdup_printf (":%s", snapd_slot_get_name (slot)); ++ else ++ value_label = g_strdup_printf ("%s:%s", snapd_slot_get_snap (slot), snapd_slot_get_name (slot)); ++ value = gs_permission_value_new (value_label); ++ gs_permission_value_add_metadata (value, "snap::snap", snapd_slot_get_snap (slot)); ++ gs_permission_value_add_metadata (value, "snap::slot", snapd_slot_get_name (slot)); ++ gs_permission_add_value (permission, value); ++ ++ if (connection != NULL && ++ g_strcmp0 (snapd_slot_get_snap (slot), snapd_connection_get_snap (connection)) == 0 && ++ g_strcmp0 (snapd_slot_get_name (slot), snapd_connection_get_name (connection)) == 0) ++ gs_permission_set_value (permission, value); ++ } ++ gs_app_add_permission (app, permission); ++ } ++ } ++ + return TRUE; + } + +@@ -1240,6 +1404,55 @@ gs_plugin_app_remove (GsPlugin *plugin, + return TRUE; + } + ++gboolean ++gs_plugin_app_set_permission (GsPlugin *plugin, ++ GsApp *app, ++ GsPermission *permission, ++ GsPermissionValue *value, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autoptr(SnapdClient) client = NULL; ++ const gchar *plug_snap, *plug_name; ++ ++ /* We can set permissions on apps we know of */ ++ if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0) ++ return TRUE; ++ ++ client = get_client (plugin, error); ++ if (client == NULL) ++ return FALSE; ++ ++ plug_snap = gs_app_get_id (app); ++ plug_name = gs_permission_get_metadata_item (permission, "snap::plug"); ++ ++ if (value != NULL) { ++ const gchar *slot_snap, *slot_name; ++ ++ slot_snap = gs_permission_value_get_metadata_item (value, "snap::snap"); ++ slot_name = gs_permission_value_get_metadata_item (value, "snap::slot"); ++ if (!snapd_client_connect_interface_sync (client, ++ plug_snap, ++ plug_name, ++ slot_snap, ++ slot_name, ++ NULL, NULL, ++ cancellable, error)) ++ return FALSE; ++ } else { ++ if (!snapd_client_disconnect_interface_sync (client, ++ plug_snap, ++ plug_name, ++ "", ++ "", ++ NULL, NULL, ++ cancellable, error)) ++ return FALSE; ++ } ++ ++ return TRUE; ++} ++ + gboolean + gs_plugin_auth_login (GsPlugin *plugin, GsAuth *auth, + GCancellable *cancellable, GError **error) +diff --git a/po/POTFILES.in b/po/POTFILES.in +index 4ed4e75e..81e24aac 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -41,6 +41,7 @@ src/gs-moderate-page.ui + src/gs-overview-page.c + src/gs-overview-page.ui + src/gs-page.c ++src/gs-permission-dialog.ui + lib/gs-plugin-loader.c + src/gs-popular-tile.c + src/gs-popular-tile.ui +diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml +index 298fcc65..44e5a28a 100644 +--- a/src/gnome-software.gresource.xml ++++ b/src/gnome-software.gresource.xml +@@ -19,6 +19,7 @@ + gs-loading-page.ui + gs-moderate-page.ui + gs-overview-page.ui ++ gs-permission-dialog.ui + gs-popular-tile.ui + gs-removal-dialog.ui + gs-repo-row.ui +diff --git a/src/gs-details-page.c b/src/gs-details-page.c +index 42e038e4..03fdc2bf 100644 +--- a/src/gs-details-page.c ++++ b/src/gs-details-page.c +@@ -39,6 +39,7 @@ + #include "gs-review-histogram.h" + #include "gs-review-dialog.h" + #include "gs-review-row.h" ++#include "gs-permission-dialog.h" + + /* the number of reviews to show before clicking the 'More Reviews' button */ + #define SHOW_NR_REVIEWS_INITIAL 4 +@@ -87,6 +88,7 @@ struct _GsDetailsPage + GtkWidget *button_install; + GtkWidget *button_remove; + GtkWidget *button_cancel; ++ GtkWidget *button_permissions; + GtkWidget *button_more_reviews; + GtkWidget *infobar_details_app_norepo; + GtkWidget *infobar_details_app_repo; +@@ -263,6 +265,7 @@ gs_details_page_switch_to (GsPage *page, gboolean scroll_up) + g_autofree gchar *text = NULL; + GtkStyleContext *sc; + GtkAdjustment *adj; ++ GPtrArray *permissions; + + if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_DETAILS) { + g_warning ("Called switch_to(details) when in mode %s", +@@ -419,6 +422,19 @@ gs_details_page_switch_to (GsPage *page, gboolean scroll_up) + gtk_widget_set_visible (self->button_remove, FALSE); + } + ++ /* permissions button */ ++ switch (gs_app_get_state (self->app)) { ++ case AS_APP_STATE_INSTALLED: ++ case AS_APP_STATE_UPDATABLE: ++ case AS_APP_STATE_UPDATABLE_LIVE: ++ permissions = gs_app_get_permissions (self->app); ++ gtk_widget_set_visible (self->button_permissions, permissions->len > 0); ++ break; ++ default: ++ gtk_widget_set_visible (self->button_permissions, FALSE); ++ break; ++ } ++ + adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_details)); + gtk_adjustment_set_value (adj, gtk_adjustment_get_lower (adj)); + +@@ -2102,6 +2118,29 @@ gs_details_page_channel_cb (GtkWidget *widget, GsDetailsPage *self) + gtk_widget_show (self->popover_channel); + } + ++static void ++gs_details_page_permission_changed_cb (GsPermissionDialog *dialog, GsPermission *permission, GsPermissionValue *value, GsDetailsPage *self) ++{ ++ g_autoptr(GCancellable) cancellable = g_cancellable_new (); ++ g_set_object (&self->cancellable, cancellable); ++ gs_page_set_app_permission (GS_PAGE (self), self->app, permission, value, self->cancellable); ++} ++ ++static void ++gs_details_page_app_permissions_button_cb (GtkWidget *widget, GsDetailsPage *self) ++{ ++ GtkWidget *dialog; ++ ++ dialog = gs_permission_dialog_new (self->app); ++ g_signal_connect (dialog, "permission-changed", ++ G_CALLBACK (gs_details_page_permission_changed_cb), self); ++ gs_shell_modal_dialog_present (self->shell, GTK_DIALOG (dialog)); ++ ++ /* just destroy */ ++ g_signal_connect_swapped (dialog, "response", ++ G_CALLBACK (gtk_widget_destroy), dialog); ++} ++ + static void + gs_details_page_app_install_button_cb (GtkWidget *widget, GsDetailsPage *self) + { +@@ -2546,6 +2585,9 @@ gs_details_page_setup (GsPage *page, + g_signal_connect (self->button_cancel, "clicked", + G_CALLBACK (gs_details_page_app_cancel_button_cb), + self); ++ g_signal_connect (self->button_permissions, "clicked", ++ G_CALLBACK (gs_details_page_app_permissions_button_cb), ++ self); + g_signal_connect (self->button_more_reviews, "clicked", + G_CALLBACK (gs_details_page_more_reviews_button_cb), + self); +@@ -2647,6 +2689,7 @@ gs_details_page_class_init (GsDetailsPageClass *klass) + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_install); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_remove); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_cancel); ++ gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_permissions); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, button_more_reviews); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, infobar_details_app_norepo); + gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, infobar_details_app_repo); +diff --git a/src/gs-details-page.ui b/src/gs-details-page.ui +index 3a6c7b02..50f04630 100644 +--- a/src/gs-details-page.ui ++++ b/src/gs-details-page.ui +@@ -367,6 +367,22 @@ + end + + ++ ++ ++ True ++ _Permissions ++ 105 ++ True ++ True ++ start ++ start ++ ++ ++ False ++ False ++ 9 ++ ++ + + + False +@@ -559,7 +575,7 @@ + + False + False +- 0 ++ 1 + + + +diff --git a/src/gs-page.c b/src/gs-page.c +index 7819523c..edbb5625 100644 +--- a/src/gs-page.c ++++ b/src/gs-page.c +@@ -779,6 +779,36 @@ gs_page_launch_app (GsPage *page, GsApp *app, GCancellable *cancellable) + NULL); + } + ++static void ++gs_page_app_permission_set_cb (GObject *source, ++ GAsyncResult *res, ++ gpointer user_data) ++{ ++ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source); ++ g_autoptr(GError) error = NULL; ++ if (!gs_plugin_loader_job_action_finish (plugin_loader, res, &error)) { ++ g_warning ("failed to set permission on GsApp: %s", error->message); ++ return; ++ } ++} ++ ++void ++gs_page_set_app_permission (GsPage *page, GsApp *app, GsPermission *permission, GsPermissionValue *value, GCancellable *cancellable) ++{ ++ GsPagePrivate *priv = gs_page_get_instance_private (page); ++ g_autoptr(GsPluginJob) plugin_job = NULL; ++ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SET_PERMISSION, ++ "app", app, ++ "failure-flags", GS_PLUGIN_FAILURE_FLAGS_USE_EVENTS, ++ "permission", permission, ++ "permission-value", value, ++ NULL); ++ gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job, ++ cancellable, ++ gs_page_app_permission_set_cb, ++ NULL); ++} ++ + static void + gs_page_app_shortcut_added_cb (GObject *source, + GAsyncResult *res, +diff --git a/src/gs-page.h b/src/gs-page.h +index 0b0d69d0..50668864 100644 +--- a/src/gs-page.h ++++ b/src/gs-page.h +@@ -79,6 +79,11 @@ void gs_page_update_app (GsPage *page, + void gs_page_launch_app (GsPage *page, + GsApp *app, + GCancellable *cancellable); ++void gs_page_set_app_permission (GsPage *page, ++ GsApp *app, ++ GsPermission *permission, ++ GsPermissionValue *value, ++ GCancellable *cancellable); + void gs_page_shortcut_add (GsPage *page, + GsApp *app, + GCancellable *cancellable); +diff --git a/src/gs-permission-combo-box.c b/src/gs-permission-combo-box.c +new file mode 100644 +index 00000000..f79e6a28 +--- /dev/null ++++ b/src/gs-permission-combo-box.c +@@ -0,0 +1,125 @@ ++/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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-permission-combo-box.h" ++ ++struct _GsPermissionComboBox ++{ ++ GtkComboBox parent_instance; ++ ++ GsPermission *permission; ++}; ++ ++G_DEFINE_TYPE (GsPermissionComboBox, gs_permission_combo_box, GTK_TYPE_COMBO_BOX) ++ ++enum { ++ SIGNAL_CHANGED, ++ SIGNAL_LAST ++}; ++ ++static guint signals [SIGNAL_LAST] = { 0 }; ++ ++GsPermission * ++gs_permission_combo_box_get_permission (GsPermissionComboBox *combo) ++{ ++ g_return_val_if_fail (GS_IS_PERMISSION_COMBO_BOX (combo), NULL); ++ return combo->permission; ++} ++ ++static void ++changed_cb (GsPermissionComboBox *combo) ++{ ++ GtkTreeIter iter; ++ GsPermissionValue *value = NULL; ++ ++ if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (combo), &iter)) ++ gtk_tree_model_get (gtk_combo_box_get_model (GTK_COMBO_BOX (combo)), &iter, 1, &value, -1); ++ ++ g_signal_emit (combo, signals[SIGNAL_CHANGED], 0, value); ++} ++ ++static void ++gs_permission_combo_box_dispose (GObject *object) ++{ ++ GsPermissionComboBox *combo = GS_PERMISSION_COMBO_BOX (object); ++ ++ g_clear_object (&combo->permission); ++ ++ G_OBJECT_CLASS (gs_permission_combo_box_parent_class)->dispose (object); ++} ++ ++static void ++gs_permission_combo_box_class_init (GsPermissionComboBoxClass *klass) ++{ ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ ++ object_class->dispose = gs_permission_combo_box_dispose; ++ ++ signals [SIGNAL_CHANGED] = ++ g_signal_new ("changed", ++ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, ++ 0, ++ NULL, NULL, g_cclosure_marshal_generic, ++ G_TYPE_NONE, 1, GS_TYPE_PERMISSION_VALUE); ++} ++ ++static void ++gs_permission_combo_box_init (GsPermissionComboBox *combo) ++{ ++} ++ ++GsPermissionComboBox * ++gs_permission_combo_box_new (GsPermission *permission) ++{ ++ GsPermissionComboBox *combo; ++ GtkListStore *store; ++ GtkCellRenderer *renderer; ++ guint i; ++ GtkTreeIter iter; ++ GPtrArray *values; ++ ++ combo = g_object_new (GS_TYPE_PERMISSION_COMBO_BOX, NULL); ++ combo->permission = g_object_ref (permission); ++ ++ store = gtk_list_store_new (2, G_TYPE_STRING, GS_TYPE_PERMISSION_VALUE); ++ gtk_combo_box_set_model (GTK_COMBO_BOX (combo), GTK_TREE_MODEL (store)); ++ ++ renderer = gtk_cell_renderer_text_new (); ++ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), renderer, TRUE); ++ gtk_cell_layout_add_attribute (GTK_CELL_LAYOUT (combo), renderer, "text", 0); ++ ++ gtk_list_store_append (store, &iter); ++ gtk_list_store_set (store, &iter, 0, "(disconnected)", 1, NULL, -1); ++ values = gs_permission_get_values (permission); ++ for (i = 0; i < values->len; i++) { ++ GsPermissionValue *value = g_ptr_array_index (values, i); ++ ++ gtk_list_store_append (store, &iter); ++ gtk_list_store_set (store, &iter, 0, gs_permission_value_get_label (value), 1, value, -1); ++ ++ if (value == gs_permission_get_value (permission)) ++ gtk_combo_box_set_active_iter (GTK_COMBO_BOX (combo), &iter); ++ } ++ ++ g_signal_connect (combo, "changed", G_CALLBACK (changed_cb), NULL); ++ ++ return combo; ++} +diff --git a/src/gs-permission-combo-box.h b/src/gs-permission-combo-box.h +new file mode 100644 +index 00000000..5b1f6470 +--- /dev/null ++++ b/src/gs-permission-combo-box.h +@@ -0,0 +1,41 @@ ++/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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_PERMISSION_COMBO_BOX_H ++#define GS_PERMISSION_COMBO_BOX_H ++ ++#include ++ ++#include "gnome-software-private.h" ++ ++G_BEGIN_DECLS ++ ++#define GS_TYPE_PERMISSION_COMBO_BOX (gs_permission_combo_box_get_type ()) ++ ++G_DECLARE_FINAL_TYPE (GsPermissionComboBox, gs_permission_combo_box, GS, PERMISSION_COMBO_BOX, GtkComboBox) ++ ++GsPermissionComboBox *gs_permission_combo_box_new (GsPermission *permission); ++ ++GsPermission *gs_permission_combo_box_get_permission (GsPermissionComboBox *combo); ++ ++G_END_DECLS ++ ++#endif /* GS_PERMISSION_COMBO_BOX_H */ +diff --git a/src/gs-permission-dialog.c b/src/gs-permission-dialog.c +new file mode 100644 +index 00000000..50c330c9 +--- /dev/null ++++ b/src/gs-permission-dialog.c +@@ -0,0 +1,153 @@ ++/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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 "gs-permission-dialog.h" ++#include "gs-permission-switch.h" ++#include "gs-permission-combo-box.h" ++ ++struct _GsPermissionDialog ++{ ++ GtkDialog parent_instance; ++ ++ GsApp *app; ++ GtkWidget *permission_grid; ++ GtkWidget *close_button; ++}; ++ ++G_DEFINE_TYPE (GsPermissionDialog, gs_permission_dialog, GTK_TYPE_DIALOG) ++ ++enum { ++ SIGNAL_PERMISSION_CHANGED, ++ SIGNAL_LAST ++}; ++ ++static guint signals [SIGNAL_LAST] = { 0 }; ++ ++static void ++close_button_clicked (GtkWidget *widget, GsPermissionDialog *dialog) ++{ ++ gtk_widget_destroy (GTK_WIDGET (dialog)); ++} ++ ++static void ++gs_permission_dialog_init (GsPermissionDialog *dialog) ++{ ++ gtk_widget_init_template (GTK_WIDGET (dialog)); ++ ++ g_signal_connect (dialog->close_button, "clicked", ++ G_CALLBACK (close_button_clicked), dialog); ++} ++ ++static void ++gs_permission_dialog_dispose (GObject *object) ++{ ++ GsPermissionDialog *dialog = GS_PERMISSION_DIALOG (object); ++ ++ g_clear_object (&dialog->app); ++ ++ G_OBJECT_CLASS (gs_permission_dialog_parent_class)->dispose (object); ++} ++ ++static void ++gs_permission_dialog_class_init (GsPermissionDialogClass *klass) ++{ ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); ++ ++ object_class->dispose = gs_permission_dialog_dispose; ++ ++ signals [SIGNAL_PERMISSION_CHANGED] = ++ g_signal_new ("permission-changed", ++ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, ++ 0, ++ NULL, NULL, g_cclosure_marshal_generic, ++ G_TYPE_NONE, 2, GS_TYPE_PERMISSION, GS_TYPE_PERMISSION_VALUE); ++ ++ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-permission-dialog.ui"); ++ ++ gtk_widget_class_bind_template_child (widget_class, GsPermissionDialog, permission_grid); ++ gtk_widget_class_bind_template_child (widget_class, GsPermissionDialog, close_button); ++} ++ ++// FIXME: Make a GsPermissionControl interfaces that can be shared between GsPermissionSwitch and GsPermissionComboBox ++ ++static void ++permission_switch_changed_cb (GsPermissionSwitch *sw, GsPermissionValue *value, GsPermissionDialog *dialog) ++{ ++ g_signal_emit (dialog, signals[SIGNAL_PERMISSION_CHANGED], 0, ++ gs_permission_switch_get_permission (sw), ++ value); ++} ++ ++static void ++permission_combo_box_changed_cb (GsPermissionComboBox *combo, GsPermissionValue *value, GsPermissionDialog *dialog) ++{ ++ g_signal_emit (dialog, signals[SIGNAL_PERMISSION_CHANGED], 0, ++ gs_permission_combo_box_get_permission (combo), ++ value); ++} ++ ++static void ++set_row (GsPermissionDialog *dialog, int row, GsPermission *permission) ++{ ++ GtkWidget *label; ++ GtkWidget *control; ++ ++ label = gtk_label_new (gs_permission_get_label (permission)); ++ gtk_label_set_xalign (GTK_LABEL (label), 1.0); ++ gtk_widget_set_hexpand (label, TRUE); ++ gtk_widget_show (label); ++ gtk_grid_attach (GTK_GRID (dialog->permission_grid), label, 0, row, 1, 1); ++ ++ if (gs_permission_get_values (permission)->len == 1) { ++ control = GTK_WIDGET (gs_permission_switch_new (permission)); ++ g_signal_connect (control, "changed", G_CALLBACK (permission_switch_changed_cb), dialog); ++ } ++ else { ++ control = GTK_WIDGET (gs_permission_combo_box_new (permission)); ++ g_signal_connect (control, "changed", G_CALLBACK (permission_combo_box_changed_cb), dialog); ++ } ++ gtk_widget_show (control); ++ gtk_grid_attach (GTK_GRID (dialog->permission_grid), control, 1, row, 1, 1); ++} ++ ++GtkWidget * ++gs_permission_dialog_new (GsApp *app) ++{ ++ GsPermissionDialog *dialog; ++ GPtrArray *permissions; ++ guint i; ++ ++ dialog = g_object_new (GS_TYPE_PERMISSION_DIALOG, ++ "use-header-bar", TRUE, ++ NULL); ++ dialog->app = g_object_ref (app); ++ ++ permissions = gs_app_get_permissions (app); ++ for (i = 0; i < permissions->len; i++) { ++ GsPermission *permission = g_ptr_array_index (permissions, i); ++ set_row (dialog, i, permission); ++ } ++ ++ return GTK_WIDGET (dialog); ++} +diff --git a/src/gs-permission-dialog.h b/src/gs-permission-dialog.h +new file mode 100644 +index 00000000..e13114df +--- /dev/null ++++ b/src/gs-permission-dialog.h +@@ -0,0 +1,39 @@ ++/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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_PERMISSION_DIALOG_H ++#define GS_PERMISSION_DIALOG_H ++ ++#include ++ ++#include "gnome-software-private.h" ++ ++G_BEGIN_DECLS ++ ++#define GS_TYPE_PERMISSION_DIALOG (gs_permission_dialog_get_type ()) ++ ++G_DECLARE_FINAL_TYPE (GsPermissionDialog, gs_permission_dialog, GS, PERMISSION_DIALOG, GtkDialog) ++ ++GtkWidget *gs_permission_dialog_new (GsApp *app); ++ ++G_END_DECLS ++ ++#endif /* GS_PERMISSION_DIALOG_H */ +diff --git a/src/gs-permission-dialog.ui b/src/gs-permission-dialog.ui +new file mode 100644 +index 00000000..e4f2955d +--- /dev/null ++++ b/src/gs-permission-dialog.ui +@@ -0,0 +1,67 @@ ++ ++ ++ ++ ++ ++ +diff --git a/src/gs-permission-switch.c b/src/gs-permission-switch.c +new file mode 100644 +index 00000000..ea41969f +--- /dev/null ++++ b/src/gs-permission-switch.c +@@ -0,0 +1,100 @@ ++/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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 "gs-permission-switch.h" ++ ++struct _GsPermissionSwitch ++{ ++ GtkSwitch parent_instance; ++ ++ GsPermission *permission; ++}; ++ ++G_DEFINE_TYPE (GsPermissionSwitch, gs_permission_switch, GTK_TYPE_SWITCH) ++ ++enum { ++ SIGNAL_CHANGED, ++ SIGNAL_LAST ++}; ++ ++static guint signals [SIGNAL_LAST] = { 0 }; ++ ++GsPermission * ++gs_permission_switch_get_permission (GsPermissionSwitch *sw) ++{ ++ g_return_val_if_fail (GS_IS_PERMISSION_SWITCH (sw), NULL); ++ return sw->permission; ++} ++ ++static void ++active_changed_cb (GsPermissionSwitch *sw) ++{ ++ GsPermissionValue *value; ++ ++ value = g_ptr_array_index (gs_permission_get_values (sw->permission), 0); ++ g_signal_emit (sw, signals[SIGNAL_CHANGED], 0, ++ gtk_switch_get_active (GTK_SWITCH (sw)) ? value : NULL); ++} ++ ++static void ++gs_permission_switch_dispose (GObject *object) ++{ ++ GsPermissionSwitch *sw = GS_PERMISSION_SWITCH (object); ++ ++ g_clear_object (&sw->permission); ++ ++ G_OBJECT_CLASS (gs_permission_switch_parent_class)->dispose (object); ++} ++ ++static void ++gs_permission_switch_class_init (GsPermissionSwitchClass *klass) ++{ ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ ++ object_class->dispose = gs_permission_switch_dispose; ++ ++ signals [SIGNAL_CHANGED] = ++ g_signal_new ("changed", ++ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, ++ 0, ++ NULL, NULL, g_cclosure_marshal_generic, ++ G_TYPE_NONE, 1, GS_TYPE_PERMISSION_VALUE); ++} ++ ++static void ++gs_permission_switch_init (GsPermissionSwitch *sw) ++{ ++} ++ ++GsPermissionSwitch * ++gs_permission_switch_new (GsPermission *permission) ++{ ++ GsPermissionSwitch *sw; ++ ++ sw = g_object_new (GS_TYPE_PERMISSION_SWITCH, NULL); ++ sw->permission = g_object_ref (permission); ++ gtk_switch_set_active (GTK_SWITCH (sw), gs_permission_get_value (permission) != NULL); ++ g_signal_connect (sw, "notify::active", G_CALLBACK (active_changed_cb), NULL); ++ ++ return sw; ++} +diff --git a/src/gs-permission-switch.h b/src/gs-permission-switch.h +new file mode 100644 +index 00000000..72406863 +--- /dev/null ++++ b/src/gs-permission-switch.h +@@ -0,0 +1,41 @@ ++/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- ++ * ++ * Copyright (C) 2017 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_PERMISSION_SWITCH_H ++#define GS_PERMISSION_SWITCH_H ++ ++#include ++ ++#include "gnome-software-private.h" ++ ++G_BEGIN_DECLS ++ ++#define GS_TYPE_PERMISSION_SWITCH (gs_permission_switch_get_type ()) ++ ++G_DECLARE_FINAL_TYPE (GsPermissionSwitch, gs_permission_switch, GS, PERMISSION_SWITCH, GtkSwitch) ++ ++GsPermissionSwitch *gs_permission_switch_new (GsPermission *permission); ++ ++GsPermission *gs_permission_switch_get_permission (GsPermissionSwitch *sw); ++ ++G_END_DECLS ++ ++#endif /* GS_PERMISSION_SWITCH_H */ +diff --git a/src/meson.build b/src/meson.build +index 60e0834c..a312e6a5 100644 +--- a/src/meson.build ++++ b/src/meson.build +@@ -43,6 +43,9 @@ gnome_software_sources = [ + 'gs-moderate-page.c', + 'gs-overview-page.c', + 'gs-page.c', ++ 'gs-permission-combo-box.c', ++ 'gs-permission-dialog.c', ++ 'gs-permission-switch.c', + 'gs-popular-tile.c', + 'gs-progress-button.c', + 'gs-removal-dialog.c', +-- +2.17.0 + diff -Nru gnome-software-3.27.90/debian/patches/0015-Don-t-reject-unexpected-state-changes-external-event.patch gnome-software-3.28.1/debian/patches/0015-Don-t-reject-unexpected-state-changes-external-event.patch --- gnome-software-3.27.90/debian/patches/0015-Don-t-reject-unexpected-state-changes-external-event.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0015-Don-t-reject-unexpected-state-changes-external-event.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,26 @@ +From 6d74f5946010d46a3c46ad778044e5dbfba9d0fe Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Fri, 9 Mar 2018 12:29:23 +0100 +Subject: [PATCH 15/18] Don't reject unexpected state changes - external events + may make us get out of sync + +--- + lib/gs-app.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/lib/gs-app.c b/lib/gs-app.c +index 018e98f4..23cd942e 100644 +--- a/lib/gs-app.c ++++ b/lib/gs-app.c +@@ -1020,7 +1020,7 @@ gs_app_set_state_internal (GsApp *app, AsAppState state) + gs_app_get_unique_id_unlocked (app), + as_app_state_to_string (priv->state), + as_app_state_to_string (state)); +- return FALSE; ++ //return FALSE; + } + + priv->state = state; +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0016-build-Translate-Ubuntu-s-.desktop-file.patch gnome-software-3.28.1/debian/patches/0016-build-Translate-Ubuntu-s-.desktop-file.patch --- gnome-software-3.27.90/debian/patches/0016-build-Translate-Ubuntu-s-.desktop-file.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0016-build-Translate-Ubuntu-s-.desktop-file.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,64 @@ +From b70195cba3afd4ef3cce69f75ffae767acedeca5 Mon Sep 17 00:00:00 2001 +From: Gunnar Hjalmarsson +Date: Thu, 22 Mar 2018 08:03:14 -0400 +Subject: [PATCH 16/18] build: Translate Ubuntu's .desktop file + +--- + data/meson.build | 3 +++ + data/org.gnome.Software.desktop | 19 +++++++++++++++++++ + po/POTFILES.in | 1 + + 3 files changed, 23 insertions(+) + create mode 100644 data/org.gnome.Software.desktop + +diff --git a/data/meson.build b/data/meson.build +index b260411b..bb6e0e1a 100644 +--- a/data/meson.build ++++ b/data/meson.build +@@ -6,6 +6,9 @@ compiled_schemas = gnome.compile_schemas() + install_data('org.gnome.software.gschema.xml', + install_dir : 'share/glib-2.0/schemas') + ++install_data('org.gnome.Software.desktop', ++ install_dir : 'share/ubuntu/applications') ++ + if get_option('enable-external-appstream') + # replace @libexecdir@ + conf_data = configuration_data() +diff --git a/data/org.gnome.Software.desktop b/data/org.gnome.Software.desktop +new file mode 100644 +index 00000000..663dc208 +--- /dev/null ++++ b/data/org.gnome.Software.desktop +@@ -0,0 +1,19 @@ ++[Desktop Entry] ++Name=Ubuntu Software ++Comment=Add, remove or update software on this computer ++# Translators: Do NOT translate or transliterate this text (this is an icon file name)! ++Icon=ubuntusoftware ++Exec=gnome-software %U ++Terminal=false ++Type=Application ++Categories=GNOME;GTK;System;PackageManager; ++# Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! ++Keywords=Updates;Upgrade;Sources;Repositories;Preferences;Install;Uninstall;Program;Software;App;Store; ++StartupNotify=true ++MimeType=x-scheme-handler/appstream;x-scheme-handler/apt;x-scheme-handler/snap; ++X-GNOME-Bugzilla-Bugzilla=GNOME ++X-GNOME-Bugzilla-Product=gnome-software ++X-GNOME-Bugzilla-Component=gnome-software ++X-GNOME-UsesNotifications=true ++DBusActivatable=true ++X-Ubuntu-Gettext-Domain=gnome-software +diff --git a/po/POTFILES.in b/po/POTFILES.in +index 81e24aac..ddcb8c13 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -1,4 +1,5 @@ + data/appdata/org.gnome.Software.appdata.xml.in ++data/org.gnome.Software.desktop + data/org.gnome.software.external-appstream.policy.in + data/org.gnome.software.gschema.xml + src/gnome-software-local-file.desktop.in +-- +2.15.1 + diff -Nru gnome-software-3.27.90/debian/patches/0017-snap-Use-default-icon-if-none-provided.patch gnome-software-3.28.1/debian/patches/0017-snap-Use-default-icon-if-none-provided.patch --- gnome-software-3.27.90/debian/patches/0017-snap-Use-default-icon-if-none-provided.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0017-snap-Use-default-icon-if-none-provided.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,93 @@ +From d5dce958b285394ab5e7a8fa233b22d4e24f1990 Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Mon, 16 Apr 2018 11:17:42 +1200 +Subject: [PATCH 17/20] snap: Use default icon if none provided + +--- + plugins/snap/default-snap-icon.svg | 1 + + plugins/snap/gs-plugin-snap.c | 17 +++++++++++++++-- + plugins/snap/gs-plugin-snap.gresource.xml | 6 ++++++ + plugins/snap/meson.build | 8 ++++++++ + 4 files changed, 30 insertions(+), 2 deletions(-) + create mode 100644 plugins/snap/default-snap-icon.svg + create mode 100644 plugins/snap/gs-plugin-snap.gresource.xml + +diff --git a/plugins/snap/default-snap-icon.svg b/plugins/snap/default-snap-icon.svg +new file mode 100644 +index 00000000..0d8dd94b +--- /dev/null ++++ b/plugins/snap/default-snap-icon.svg +@@ -0,0 +1 @@ ++ +\ No newline at end of file +diff --git a/plugins/snap/gs-plugin-snap.c b/plugins/snap/gs-plugin-snap.c +index ced103d7..f229fbf0 100644 +--- a/plugins/snap/gs-plugin-snap.c ++++ b/plugins/snap/gs-plugin-snap.c +@@ -799,6 +799,9 @@ load_store_icon (GsApp *app, SnapdSnap *snap) + static gboolean + load_icon (GsPlugin *plugin, SnapdClient *client, GsApp *app, const gchar *id, SnapdSnap *local_snap, SnapdSnap *store_snap, GCancellable *cancellable) + { ++ g_autoptr(GdkPixbuf) pixbuf = NULL; ++ g_autoptr(GError) error = NULL; ++ + if (local_snap != NULL) { + if (load_snap_icon (app, client, local_snap, cancellable)) + return TRUE; +@@ -808,9 +811,19 @@ load_icon (GsPlugin *plugin, SnapdClient *client, GsApp *app, const gchar *id, S + + if (store_snap == NULL) + store_snap = get_store_snap (plugin, id, FALSE, cancellable, NULL); +- if (store_snap != NULL) +- return load_store_icon (app, store_snap); ++ if (store_snap != NULL) { ++ if (load_store_icon (app, store_snap)) ++ return TRUE; ++ } ++ ++ /* Default to built-in icon */ ++ pixbuf = gdk_pixbuf_new_from_resource_at_scale ("/org/gnome/Software/Snap/default-snap-icon.svg", 64, 64, TRUE, &error); ++ if (pixbuf != NULL) { ++ gs_app_set_pixbuf (app, pixbuf); ++ return TRUE; ++ } + ++ g_warning ("Failed to load built-in icon: %s", error->message); + return FALSE; + } + +diff --git a/plugins/snap/gs-plugin-snap.gresource.xml b/plugins/snap/gs-plugin-snap.gresource.xml +new file mode 100644 +index 00000000..6a35050a +--- /dev/null ++++ b/plugins/snap/gs-plugin-snap.gresource.xml +@@ -0,0 +1,6 @@ ++ ++ ++ ++ default-snap-icon.svg ++ ++ +diff --git a/plugins/snap/meson.build b/plugins/snap/meson.build +index c44d13e7..b29102b2 100644 +--- a/plugins/snap/meson.build ++++ b/plugins/snap/meson.build +@@ -1,7 +1,15 @@ + cargs = ['-DG_LOG_DOMAIN="GsPluginSnap"'] + ++resources_src = gnome.compile_resources( ++ 'gs-plugin-snap-resources', ++ 'gs-plugin-snap.gresource.xml', ++ source_dir : '.', ++ c_name : 'gs_plugin_snap' ++) ++ + shared_module( + 'gs_plugin_snap', ++ resources_src, + sources : [ + 'gs-plugin-snap.c' + ], +-- +2.17.0 + diff -Nru gnome-software-3.27.90/debian/patches/0018-snap-Make-snaps-purchasable.patch gnome-software-3.28.1/debian/patches/0018-snap-Make-snaps-purchasable.patch --- gnome-software-3.27.90/debian/patches/0018-snap-Make-snaps-purchasable.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0018-snap-Make-snaps-purchasable.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,125 @@ +From 3d554c77c82ec698b7065714a4ffa46907251b71 Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Wed, 24 Jan 2018 15:53:56 +1300 +Subject: [PATCH 18/20] snap: Make snaps purchasable + +--- + plugins/snap/gs-plugin-snap.c | 66 ++++++++++++++++++++++++++++++++--- + 1 file changed, 62 insertions(+), 4 deletions(-) + +diff --git a/plugins/snap/gs-plugin-snap.c b/plugins/snap/gs-plugin-snap.c +index f229fbf0..56bcea6d 100644 +--- a/plugins/snap/gs-plugin-snap.c ++++ b/plugins/snap/gs-plugin-snap.c +@@ -148,6 +148,14 @@ snapd_error_convert (GError **perror) + case SNAPD_ERROR_TWO_FACTOR_INVALID: + error->code = GS_PLUGIN_ERROR_AUTH_INVALID; + break; ++ case SNAPD_ERROR_PAYMENT_NOT_SETUP: ++ error->code = GS_PLUGIN_ERROR_PURCHASE_NOT_SETUP; ++ g_free (error->message); ++ error->message = g_strdup ("do online using @https://my.ubuntu.com/payment/edit"); ++ break; ++ case SNAPD_ERROR_PAYMENT_DECLINED: ++ error->code = GS_PLUGIN_ERROR_PURCHASE_DECLINED; ++ break; + case SNAPD_ERROR_CONNECTION_FAILED: + case SNAPD_ERROR_WRITE_FAILED: + case SNAPD_ERROR_READ_FAILED: +@@ -156,8 +164,6 @@ snapd_error_convert (GError **perror) + case SNAPD_ERROR_PERMISSION_DENIED: + case SNAPD_ERROR_FAILED: + case SNAPD_ERROR_TERMS_NOT_ACCEPTED: +- case SNAPD_ERROR_PAYMENT_NOT_SETUP: +- case SNAPD_ERROR_PAYMENT_DECLINED: + case SNAPD_ERROR_ALREADY_INSTALLED: + case SNAPD_ERROR_NOT_INSTALLED: + case SNAPD_ERROR_NO_UPDATE_AVAILABLE: +@@ -336,6 +342,7 @@ snap_to_app (GsPlugin *plugin, SnapdSnap *snap) + else + app = g_object_ref (cached_app); + ++ gs_app_set_metadata (app, "snap::id", snapd_snap_get_id (snap)); + gs_app_set_management_plugin (app, "snap"); + gs_app_add_quirk (app, AS_APP_QUIRK_NOT_REVIEWABLE); + if (gs_app_get_kind (app) != AS_APP_KIND_DESKTOP) +@@ -971,8 +978,12 @@ gs_plugin_refine_app (GsPlugin *plugin, + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + } + } +- else +- gs_app_set_state (app, AS_APP_STATE_AVAILABLE); ++ else { ++ if (store_snap != NULL && snapd_snap_get_status (store_snap) == SNAPD_SNAP_STATUS_PRICED) ++ gs_app_set_state (app, AS_APP_STATE_PURCHASABLE); ++ else ++ gs_app_set_state (app, AS_APP_STATE_AVAILABLE); ++ } + + /* use store information for basic metadata over local information */ + snap = store_snap != NULL ? store_snap : local_snap; +@@ -1008,7 +1019,16 @@ gs_plugin_refine_app (GsPlugin *plugin, + + /* add information specific to store snaps */ + if (store_snap != NULL) { ++ GPtrArray *prices; ++ + gs_app_set_origin (app, priv->store_name); ++ ++ prices = snapd_snap_get_prices (store_snap); ++ if (prices->len > 0) { ++ SnapdPrice *price = prices->pdata[0]; ++ gs_app_set_price (app, snapd_price_get_amount (price), snapd_price_get_currency (price)); ++ } ++ + gs_app_set_size_download (app, snapd_snap_get_download_size (store_snap)); + + if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS && gs_app_get_screenshots (app)->len == 0) { +@@ -1213,6 +1233,44 @@ gs_plugin_refine_app (GsPlugin *plugin, + return TRUE; + } + ++gboolean ++gs_plugin_app_purchase (GsPlugin *plugin, ++ GsApp *app, ++ GsPrice *price, ++ GCancellable *cancellable, ++ GError **error) ++{ ++ g_autoptr(SnapdClient) client = NULL; ++ const gchar *id; ++ ++ /* We can only purchase apps we know of */ ++ if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0) ++ return TRUE; ++ ++ client = get_client (plugin, error); ++ if (client == NULL) ++ return FALSE; ++ ++ gs_app_set_state (app, AS_APP_STATE_PURCHASING); ++ ++ if (!snapd_client_check_buy_sync (client, cancellable, error)) { ++ gs_app_set_state_recover (app); ++ snapd_error_convert (error); ++ return FALSE; ++ } ++ ++ id = gs_app_get_metadata_item (app, "snap::id"); ++ if (!snapd_client_buy_sync (client, id, gs_price_get_amount (price), gs_price_get_currency (price), cancellable, error)) { ++ gs_app_set_state_recover (app); ++ snapd_error_convert (error); ++ return FALSE; ++ } ++ ++ gs_app_set_state (app, AS_APP_STATE_AVAILABLE); ++ ++ return TRUE; ++} ++ + static void + progress_cb (SnapdClient *client, SnapdChange *change, gpointer deprecated, gpointer user_data) + { +-- +2.17.0 + diff -Nru gnome-software-3.27.90/debian/patches/0019-Disable-paid-snap-support-unless-env-variable-GNOME_.patch gnome-software-3.28.1/debian/patches/0019-Disable-paid-snap-support-unless-env-variable-GNOME_.patch --- gnome-software-3.27.90/debian/patches/0019-Disable-paid-snap-support-unless-env-variable-GNOME_.patch 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/0019-Disable-paid-snap-support-unless-env-variable-GNOME_.patch 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,33 @@ +From 965c0ab151695c119ab7f01f14112322a961ecc6 Mon Sep 17 00:00:00 2001 +From: Robert Ancell +Date: Thu, 19 Apr 2018 15:33:42 +1200 +Subject: [PATCH 19/21] Disable paid snap support unless env variable + GNOME_SOFTWARE_SHOW_PAID is set + +--- + plugins/snap/gs-plugin-snap.c | 8 +++++++- + 1 file changed, 7 insertions(+), 1 deletion(-) + +diff --git a/plugins/snap/gs-plugin-snap.c b/plugins/snap/gs-plugin-snap.c +index 870c4282..09c163cd 100644 +--- a/plugins/snap/gs-plugin-snap.c ++++ b/plugins/snap/gs-plugin-snap.c +@@ -975,8 +975,14 @@ gs_plugin_refine_app (GsPlugin *plugin, + } + } + else { +- if (store_snap != NULL && snapd_snap_get_status (store_snap) == SNAPD_SNAP_STATUS_PRICED) ++ if (store_snap != NULL && snapd_snap_get_status (store_snap) == SNAPD_SNAP_STATUS_PRICED) { ++ if (g_getenv ("GNOME_SOFTWARE_SHOW_PAID") == NULL) { ++ g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, "Paid snaps not supported"); ++ return FALSE; ++ } ++ + gs_app_set_state (app, AS_APP_STATE_PURCHASABLE); ++ } + else + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + } +-- +2.17.0 + diff -Nru gnome-software-3.27.90/debian/patches/series gnome-software-3.28.1/debian/patches/series --- gnome-software-3.27.90/debian/patches/series 2018-02-18 21:27:18.000000000 +0000 +++ gnome-software-3.28.1/debian/patches/series 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,25 @@ +# Patches from upstream +0001-snap-Scale-embedded-snap-icons-to-64x64.patch +0001-snap-Only-load-icons-once.patch +0001-Fix-gs_app_set_state_recover-not-working-during-purc.patch + +# Patches from the ubuntu* branch +0001-Construct-the-Software-Sources-menu-item-dynamically.patch +0002-Download-changelog-information-on-demand-this-stops-.patch +0003-Sort-snaps-before-other-apps.patch +0004-Hide-Kudo-details-since-we-don-t-have-good-data.patch +0005-details-Show-an-in-app-notification-when-passed-an-i.patch +0006-packagekit-Disable-updates.patch +0007-snap-Only-feature-snaps.patch +0008-Don-t-randomize-editors-picks.patch +0009-Display-a-warning-for-non-sandboxed-snaps.patch +0010-Sort-category-snaps-before-other-packages.patch +0011-Support-snap-channels.patch +0012-Don-t-use-colour-to-differentiate-between-free-and-p.patch +0013-overview-page-Rotate-featured-apps.patch +0014-Add-a-basic-permissions-system.patch +0015-Don-t-reject-unexpected-state-changes-external-event.patch +0016-build-Translate-Ubuntu-s-.desktop-file.patch +0017-snap-Use-default-icon-if-none-provided.patch +0018-snap-Make-snaps-purchasable.patch +0019-Disable-paid-snap-support-unless-env-variable-GNOME_.patch diff -Nru gnome-software-3.27.90/debian/ubuntu-software.install gnome-software-3.28.1/debian/ubuntu-software.install --- gnome-software-3.27.90/debian/ubuntu-software.install 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/ubuntu-software.install 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,2 @@ +usr/share/ubuntu/applications/ +debian/icons/hicolor usr/share/icons/ diff -Nru gnome-software-3.27.90/debian/ubuntu-software.links gnome-software-3.28.1/debian/ubuntu-software.links --- gnome-software-3.27.90/debian/ubuntu-software.links 1970-01-01 00:00:00.000000000 +0000 +++ gnome-software-3.28.1/debian/ubuntu-software.links 2018-04-19 03:22:29.000000000 +0000 @@ -0,0 +1,2 @@ +usr/bin/gnome-software usr/bin/ubuntu-software +usr/share/ubuntu/applications/org.gnome.Software.desktop usr/share/ubuntu-wayland/applications/org.gnome.Software.desktop