diff --git a/src/Interfaces/FwupdManager.vala b/src/Interfaces/FwupdManager.vala new file mode 100644 index 000000000..bced866e5 --- /dev/null +++ b/src/Interfaces/FwupdManager.vala @@ -0,0 +1,304 @@ +/* +* Copyright (c) 2020 elementary, Inc. (https://elementary.io) +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA +* +* Authored by: Marius Meisenzahl +*/ + +public class About.FwupdManager : Object { + [DBus (name = "org.freedesktop.fwupd")] + private interface FwupdInterface : Object { + public abstract signal void device_added (GLib.HashTable device); + public abstract signal void device_removed (GLib.HashTable device); + + public abstract async GLib.HashTable[] get_devices () throws GLib.Error; + public abstract async GLib.HashTable[] get_releases (string device_id) throws GLib.Error; + public abstract async void install (string id, UnixInputStream handle, GLib.HashTable options) throws GLib.Error; + public abstract async GLib.HashTable[] get_details (UnixInputStream handle) throws GLib.Error; + } + + private FwupdInterface fwupd; + + public signal void on_device_added (Fwupd.Device device); + public signal void on_device_error (Fwupd.Device device, string error); + public signal void on_device_removed (Fwupd.Device device); + + public async List get_devices () { + var devices_list = new List (); + + try { + var result = yield fwupd.get_devices (); + foreach (unowned GLib.HashTable device in result) { + devices_list.append (yield parse_device (device)); + } + } catch (Error e) { + warning ("Could not get devices: %s", e.message); + } + + return devices_list; + } + + private async List get_releases (string id) { + var releases_list = new List (); + + try { + var result = yield fwupd.get_releases (id); + foreach (unowned GLib.HashTable release in result) { + releases_list.append (parse_release (release)); + } + } catch (Error e) { + warning ("Could not get releases for “%s”: %s", id, e.message); + } + + return releases_list; + } + + private async Fwupd.Device parse_device (GLib.HashTable serialized_device) { + var device = new Fwupd.Device (); + + serialized_device.@foreach ((key, val) => { + switch (key) { + case "DeviceId": + device.id = val.get_string (); + break; + case "Flags": + device.flags = (Fwupd.DeviceFlag) val.get_uint64 (); + break; + case "Name": + device.name = val.get_string (); + break; + case "Summary": + device.summary = val.get_string (); + break; + case "Vendor": + device.vendor = val.get_string (); + break; + case "VendorId": + if (device.vendor == null) { + device.vendor = val.get_string (); + } + break; + case "Version": + device.version = val.get_string (); + break; + case "Icon": + var icons = val.get_strv (); + device.icon = icons.length > 0 ? icons[0] : "application-x-firmware"; + break; + case "Guid": + device.guids = val.get_strv (); + break; + case "InstallDuration": + device.install_duration = val.get_uint32 (); + break; + case "UpdateError": + device.update_error = val.get_string (); + break; + default: + break; + } + }); + + if (device.id.length > 0 && device.has_flag (Fwupd.DeviceFlag.UPDATABLE)) { + device.releases = yield get_releases (device.id); + } else { + device.releases = new List (); + } + + return device; + } + + private Fwupd.Release parse_release (GLib.HashTable serialized_release) { + var release = new Fwupd.Release () { + icon = "security-high" + }; + + serialized_release.@foreach ((key, val) => { + switch (key) { + case "Filename": + release.filename = val.get_string (); + break; + case "Name": + release.name = val.get_string (); + break; + case "Summary": + release.summary = val.get_string (); + break; + case "Version": + release.version = val.get_string (); + break; + case "Description": + release.description = val.get_string () + .replace ("

", "") + .replace ("

", "\n\n") + .replace ("
  • ", " • ") + .replace ("
  • ", "\n") + .replace ("
      ", "") + .replace ("
    ", "\n") + .replace ("
      ", "") // TODO: add support for ordered lists + .replace ("
    ", "\n") + .strip (); + break; + case "Protocol": + release.protocol = val.get_string (); + break; + case "RemoteId": + release.remote_id = val.get_string (); + break; + case "AppstreamId": + release.appstream_id = val.get_string (); + break; + case "Checksum": + release.checksum = val.get_string (); + break; + case "Vendor": + release.vendor = val.get_string (); + break; + case "Size": + release.size = val.get_uint64 (); + break; + case "License": + release.license = val.get_string (); + break; + case "TrustFlags": + release.flag = (Fwupd.ReleaseFlag) val.get_uint64 (); + break; + case "InstallDuration": + release.install_duration = val.get_uint32 (); + break; + case "Uri": + release.uri = val.get_string (); + break; + default: + break; + } + }); + + return release; + } + + public async string? download_file (Fwupd.Device device, string uri) { + File server_file = File.new_for_uri (uri); + var path = Path.build_filename (Environment.get_tmp_dir (), server_file.get_basename ()); + File local_file = File.new_for_path (path); + + bool result; + try { + result = yield server_file.copy_async (local_file, FileCopyFlags.OVERWRITE, Priority.DEFAULT, null, (current_num_bytes, total_num_bytes) => { + // TODO: provide useful information for user + }); + } catch (Error e) { + on_device_error (device, "Could not download file: %s".printf (e.message)); + return null; + } + + if (!result) { + on_device_error (device, "Download of %s was not succesfull".printf (uri)); + return null; + } + + return path; + } + + public async bool install (Fwupd.Device device, string path) { + int fd; + + try { + // https://github.com/fwupd/fwupd/blob/c0d4c09a02a40167e9de57f82c0033bb92e24167/libfwupd/fwupd-client.c#L2045 + var options = new GLib.HashTable (str_hash, str_equal); + options.insert ("reason", new Variant.string ("user-action")); + options.insert ("filename", new Variant.string (path)); + options.insert ("allow-older", new Variant.boolean (true)); + options.insert ("allow-reinstall", new Variant.boolean (true)); + options.insert ("no-history", new Variant.boolean (true)); + + fd = Posix.open (path, Posix.O_RDONLY); + var handle = new UnixInputStream (fd, true); + + yield fwupd.install (device.id, handle, options); + } catch (Error e) { + warning ("Could not install release for “%s”: %s", device.id, e.message); + on_device_error (device, device.update_error != null ? device.update_error : e.message); + return false; + } finally { + Posix.close (fd); + } + + return true; + } + + public async Fwupd.Details get_release_details (Fwupd.Device device, string path) { + var details = new Fwupd.Details (); + + int fd; + try { + fd = Posix.open (path, Posix.O_RDONLY); + var handle = new UnixInputStream (fd, true); + + var result = yield fwupd.get_details (handle); + foreach (unowned GLib.HashTable serialized_details in result) { + serialized_details.@foreach ((key, val) => { + if (key == "Release") { + var iter = val.iterator ().next_value ().iterator (); + string details_key; + Variant details_val; + while (iter.next ("{sv}", out details_key, out details_val)) { + if (details_key == "DetachCaption") { + details.caption = details_val.get_string (); + } else if (details_key == "DetachImage") { + details.image = details_val.get_string (); + } + } + } + }); + } + } catch (Error e) { + warning ("Could not get details for “%s”: %s", device.id, e.message); + on_device_error (device, device.update_error); + } finally { + Posix.close (fd); + } + + if (details.image != null) { + details.image = yield download_file (device, details.image); + } + + return details; + } + + construct { + try { + fwupd = Bus.get_proxy_sync (BusType.SYSTEM, "org.freedesktop.fwupd", "/"); + + fwupd.device_added.connect ((serialized_device) => { + parse_device.begin (serialized_device, (obj, res) => { + var device = parse_device.end (res); + on_device_added (device); + }); + }); + + fwupd.device_removed.connect ((serialized_device) => { + parse_device.begin (serialized_device, (obj, res) => { + var device = parse_device.end (res); + on_device_removed (device); + }); + }); + } catch (Error e) { + warning ("Could not connect to system bus: %s", e.message); + } + } +} diff --git a/src/Interfaces/LoginManager.vala b/src/Interfaces/LoginManager.vala new file mode 100644 index 000000000..c5bea29ca --- /dev/null +++ b/src/Interfaces/LoginManager.vala @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2021 elementary, Inc. (https://elementary.io) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * Authored by: Marius Meisenzahl + */ + +[DBus (name = "org.freedesktop.login1.Manager")] +public interface About.LoginInterface : Object { + public abstract void reboot (bool interactive) throws GLib.Error; + public abstract void power_off (bool interactive) throws GLib.Error; +} + +public class About.LoginManager : Object { + private LoginInterface interface; + + static LoginManager? instance = null; + public static LoginManager get_instance () { + if (instance == null) { + instance = new LoginManager (); + } + + return instance; + } + + private LoginManager () {} + + construct { + try { + interface = Bus.get_proxy_sync (BusType.SYSTEM, "org.freedesktop.login1", "/org/freedesktop/login1"); + } catch (Error e) { + warning ("Could not connect to login interface: %s", e.message); + } + } + + public bool shutdown () { + try { + interface.power_off (true); + } catch (Error e) { + warning ("Could not connect to login interface: %s", e.message); + return false; + } + + return true; + } + + public bool reboot () { + try { + interface.reboot (true); + } catch (Error e) { + warning ("Could not connect to login interface: %s", e.message); + return false; + } + + return true; + } +} diff --git a/src/Objects/Details.vala b/src/Objects/Details.vala new file mode 100644 index 000000000..770a1952c --- /dev/null +++ b/src/Objects/Details.vala @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 elementary, Inc. (https://elementary.io) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * Authored by: Marius Meisenzahl + */ + +public class Fwupd.Details : Object { + public string caption { get; set; } + public string image { get; set; } +} diff --git a/src/Objects/Device.vala b/src/Objects/Device.vala new file mode 100644 index 000000000..87a4dd3a4 --- /dev/null +++ b/src/Objects/Device.vala @@ -0,0 +1,40 @@ +/* +* Copyright (c) 2020 elementary, Inc. (https://elementary.io) +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA +* +* Authored by: Marius Meisenzahl +*/ + +public class Fwupd.Device : Object { + public string id { get; set; } + public string name { get; set; } + public string summary { get; set; } + public string icon { get; set; } + public string vendor { get; set; } + public string version { get; set; } + public string[] guids { get; set; } + public DeviceFlag flags { get; set; } + public uint32 install_duration { get; set; } + public string update_error { get; set; } + + public List releases { get; owned set; } + public Release latest_release { get { return releases.nth_data (0); }} + + public bool has_flag (Fwupd.DeviceFlag flag) { + return flag in flags; + } +} diff --git a/src/Objects/DeviceFlag.vala b/src/Objects/DeviceFlag.vala new file mode 100644 index 000000000..e8921539c --- /dev/null +++ b/src/Objects/DeviceFlag.vala @@ -0,0 +1,225 @@ +/* +* Copyright (c) 2020 elementary, Inc. (https://elementary.io) +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA +* +* Authored by: Marius Meisenzahl +*/ + +// https://github.com/fwupd/fwupd/blob/72df1147933de747312aa7c9892f07e7916b8a39/libfwupd/fwupd-enums.h#L133 +[Flags] +public enum Fwupd.DeviceFlag { + NONE = (0u), /* Since: 0.1.3 */ + INTERNAL = (1u << 0), /* Since: 0.1.3 */ + UPDATABLE = (1u << 1), /* Since: 0.9.7 */ + ONLY_OFFLINE = (1u << 2), /* Since: 0.9.7 */ + REQUIRE_AC = (1u << 3), /* Since: 0.6.3 */ + LOCKED = (1u << 4), /* Since: 0.6.3 */ + SUPPORTED = (1u << 5), /* Since: 0.7.1 */ + NEEDS_BOOTLOADER = (1u << 6), /* Since: 0.7.3 */ + REGISTERED = (1u << 7), /* Since: 0.9.7 */ + NEEDS_REBOOT = (1u << 8), /* Since: 0.9.7 */ + REPORTED = (1u << 9), /* Since: 1.0.4 */ + NOTIFIED = (1u << 10), /* Since: 1.0.5 */ + USE_RUNTIME_VERSION = (1u << 11), /* Since: 1.0.6 */ + INSTALL_PARENT_FIRST = (1u << 12), /* Since: 1.0.8 */ + IS_BOOTLOADER= (1u << 13), /* Since: 1.0.8 */ + WAIT_FOR_REPLUG = (1u << 14), /* Since: 1.1.2 */ + IGNORE_VALIDATION = (1u << 15), /* Since: 1.1.2 */ + TRUSTED = (1u << 16), /* Since: 1.1.2 */ + NEEDS_SHUTDOWN = (1u << 17), /* Since: 1.2.4 */ + ANOTHER_WRITE_REQUIRED = (1u << 18), /* Since: 1.2.5 */ + NO_AUTO_INSTANCE_IDS = (1u << 19), /* Since: 1.2.5 */ + NEEDS_ACTIVATION = (1u << 20), /* Since: 1.2.6 */ + ENSURE_SEMVER= (1u << 21), /* Since: 1.2.9 */ + HISTORICAL = (1u << 22), /* Since: 1.3.2 */ + ONLY_SUPPORTED = (1u << 23), /* Since: 1.3.3 */ + WILL_DISAPPEAR = (1u << 24), /* Since: 1.3.3 */ + CAN_VERIFY = (1u << 25), /* Since: 1.3.3 */ + CAN_VERIFY_IMAGE = (1u << 26), /* Since: 1.3.3 */ + DUAL_IMAGE = (1u << 27), /* Since: 1.3.3 */ + SELF_RECOVERY= (1u << 28), /* Since: 1.3.3 */ + USABLE_DURING_UPDATE = (1u << 29), /* Since: 1.3.3 */ + VERSION_CHECK_REQUIRED = (1u << 30), /* Since: 1.3.7 */ + INSTALL_ALL_RELEASES = (1u << 31), /* Since: 1.3.7 */ + MD_SET_NAME= (1u << 32), /* Since: 1.4.0 */ + MD_SET_NAME_CATEGORY = (1u << 33), /* Since: 1.4.0 */ + MD_SET_VERFMT= (1u << 34), /* Since: 1.4.0 */ + ADD_COUNTERPART_GUIDS= (1u << 35), /* Since: 1.4.0 */ + NO_GUID_MATCHING = (1u << 36), /* Since: 1.4.1 */ + UPDATABLE_HIDDEN = (1u << 37), /* Since: 1.4.1 */ + SKIPS_RESTART= (1u << 38), /* Since: 1.5.0 */ + HAS_MULTIPLE_BRANCHES = (1u << 39), /* Since: 1.5.0 */ + BACKUP_BEFORE_INSTALL = (1u << 40), /* Since: 1.5.0 */ + MD_SET_ICON = (1u << 41), /* Since: 1.5.2 */ + UNKNOWN = uint64.MAX; /* Since: 0.7.3 */ + + // https://gitlab.gnome.org/hughsie/gnome-firmware-updater/-/blob/f5281078e3cfade7ff919c812ba63de22431aaf2/src/gfu-common.c#L249 + public string? to_string () { + switch (this) { + case NONE: + return null; + case INTERNAL: + /// TRANSLATORS: Device cannot be removed easily + return _("Internal device"); + case UPDATABLE: + /// TRANSLATORS: Device is updatable in this or any other mode + return _("Updatable"); + case ONLY_OFFLINE: + /// TRANSLATORS: Update can only be done from offline mode + return _("Update requires restarting the system"); + case REQUIRE_AC: + /// TRANSLATORS: Must be plugged in to an outlet + return _("System requires external power source"); + case LOCKED: + /// TRANSLATORS: Is locked and can be unlocked + return _("Device is locked"); + case SUPPORTED: + /// TRANSLATORS: Is found in current metadata + return _("Supported on LVFS"); + case NEEDS_BOOTLOADER: + /// TRANSLATORS: Requires a bootloader mode to be manually enabled by the user + return _("Requires a bootloader"); + case NEEDS_REBOOT: + /// TRANSLATORS: Requires a reboot to apply firmware or to reload hardware + return _("Update requires restarting the system"); + case NEEDS_SHUTDOWN: + /// TRANSLATORS: Requires system shutdown to apply firmware + return _("Requires the system to shut down after installation"); + case REPORTED: + /// TRANSLATORS: Has been reported to a metadata server + return _("Reported to LVFS"); + case NOTIFIED: + /// TRANSLATORS: User has been notified + return _("User has been notified"); + case USE_RUNTIME_VERSION: + /* skip */ + return null; + case INSTALL_PARENT_FIRST: + /// TRANSLATORS: Install composite firmware on the parent before the child + return _("Install to parent device first"); + case IS_BOOTLOADER: + /// TRANSLATORS: Is currently in bootloader mode + return _("Is in bootloader mode"); + case WAIT_FOR_REPLUG: + /// TRANSLATORS: The hardware is waiting to be replugged + return _("Hardware is waiting to be replugged"); + case IGNORE_VALIDATION: + /// TRANSLATORS: Ignore validation safety checks when flashing this device + return _("Ignore validation safety checks"); + case ANOTHER_WRITE_REQUIRED: + /* skip */ + return null; + case NO_AUTO_INSTANCE_IDS: + /* skip */ + return null; + case NEEDS_ACTIVATION: + /// TRANSLATORS: Device update needs to be separately activated + return _("Device update needs activation"); + case ENSURE_SEMVER: + /* skip */ + return null; + case HISTORICAL: + /* skip */ + return null; + case ONLY_SUPPORTED: + /* skip */ + return null; + case WILL_DISAPPEAR: + /// TRANSLATORS: Device will not return after update completes + return _("Device will not re-appear after update completes"); + case CAN_VERIFY: + /// TRANSLATORS: Device supports some form of checksum verification + return _("Cryptographic hash verification is available"); + case CAN_VERIFY_IMAGE: + /* skip */ + return null; + case DUAL_IMAGE: + /// TRANSLATORS: Device supports a safety mechanism for flashing + return _("Device stages updates"); + case SELF_RECOVERY: + /// TRANSLATORS: Device supports a safety mechanism for flashing + return _("Device can recover flash failures"); + case USABLE_DURING_UPDATE: + /// TRANSLATORS: Device remains usable during update + return _("Device is usable for the duration of the update"); + case UNKNOWN: + return null; + default: + return null; + } + } + + // https://gitlab.gnome.org/hughsie/gnome-firmware-updater/-/blob/f5281078e3cfade7ff919c812ba63de22431aaf2/src/gfu-common.c#L377 + public string to_icon () { + switch (this) { + case INTERNAL: + return "drive-harddisk-symbolic"; + case UPDATABLE: + return "software-update-available-symbolic"; + case ONLY_OFFLINE: + return "network-offline-symbolic"; + case REQUIRE_AC: + return "battery-symbolic"; + case LOCKED: + return "locked-symbolic"; + case SUPPORTED: + return "security-high-symbolic"; + case NEEDS_BOOTLOADER: + return "computer-symbolic"; + case NEEDS_REBOOT: + return "system-reboot-symbolic"; + case NEEDS_SHUTDOWN: + return "system-shutdown-symbolic"; + case REPORTED: + return "task-due-symbolic"; + case NOTIFIED: + return "task-due-symbolic"; + case USE_RUNTIME_VERSION: + return "system-run-symbolic"; + case INSTALL_PARENT_FIRST: + return "system-software-install-symbolic"; + case IS_BOOTLOADER: + return "computer-symbolic"; + case WAIT_FOR_REPLUG: + return "battery-low-symbolic"; + case IGNORE_VALIDATION: + return "dialog-error-symbolic"; + case ANOTHER_WRITE_REQUIRED: + return "media-floppy-symbolic"; + case NO_AUTO_INSTANCE_IDS: + return "dialog-error-symbolic"; + case NEEDS_ACTIVATION: + return "emblem-important-symbolic"; + case ENSURE_SEMVER: + return "emblem-important-symbolic"; + case WILL_DISAPPEAR: + return "emblem-important-symbolic"; + case CAN_VERIFY: + return "emblem-important-symbolic"; + case DUAL_IMAGE: + return "emblem-important-symbolic"; + case SELF_RECOVERY: + return "emblem-important-symbolic"; + case USABLE_DURING_UPDATE: + return "emblem-important-symbolic"; + case UNKNOWN: + return "unknown-symbolic"; + default: + return "unknown-symbolic"; + } + } +} diff --git a/src/Objects/Release.vala b/src/Objects/Release.vala new file mode 100644 index 000000000..16920fe98 --- /dev/null +++ b/src/Objects/Release.vala @@ -0,0 +1,39 @@ +/* +* Copyright (c) 2020 elementary, Inc. (https://elementary.io) +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA +* +* Authored by: Marius Meisenzahl +*/ + +public class Fwupd.Release : Object { + public string filename { get; set; } + public string name { get; set; } + public string summary { get; set; } + public string icon { get; set; } + public string version { get; set; } + public string description { get; set; } + public string protocol { get; set; } + public string remote_id { get; set; } + public string appstream_id { get; set; } + public string checksum { get; set; } + public string vendor { get; set; } + public uint64 size { get; set; } + public string license { get; set; } + public ReleaseFlag flag { get; set; } + public uint32 install_duration { get; set; } + public string uri { get; set; } +} diff --git a/src/Objects/ReleaseFlag.vala b/src/Objects/ReleaseFlag.vala new file mode 100644 index 000000000..f366b6cd4 --- /dev/null +++ b/src/Objects/ReleaseFlag.vala @@ -0,0 +1,34 @@ +/* +* Copyright (c) 2020 elementary, Inc. (https://elementary.io) +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA +* +* Authored by: Marius Meisenzahl +*/ + +// https://github.com/fwupd/fwupd/blob/72df1147933de747312aa7c9892f07e7916b8a39/libfwupd/fwupd-enums.h#L192 +[Flags] +public enum Fwupd.ReleaseFlag { + NONE = (0u), /* Since: 1.2.6 */ + TRUSTED_PAYLOAD = (1u << 0), /* Since: 1.2.6 */ + TRUSTED_METADATA = (1u << 1), /* Since: 1.2.6 */ + IS_UPGRADE = (1u << 2), /* Since: 1.2.6 */ + IS_DOWNGRADE = (1u << 3), /* Since: 1.2.6 */ + BLOCKED_VERSION = (1u << 4), /* Since: 1.2.6 */ + BLOCKED_APPROVAL = (1u << 5), /* Since: 1.2.6 */ + IS_ALTERNATE_BRANCH = (1u << 6), /* Since: 1.5.0 */ + UNKNOWN = uint64.MAX; /* Since: 1.2.6 */ +} diff --git a/src/Plug.vala b/src/Plug.vala index 78b4e58e3..c2f7cf4a6 100644 --- a/src/Plug.vala +++ b/src/Plug.vala @@ -40,11 +40,14 @@ public class About.Plug : Switchboard.Plug { valign = Gtk.Align.CENTER }; + var firmware_view = new FirmwareView (); + var stack = new Gtk.Stack () { vexpand = true }; stack.add_titled (operating_system_view, "operating-system-view", _("Operating System")); stack.add_titled (hardware_view, "hardware-view", _("Hardware")); + stack.add_titled (firmware_view, "firmware-view", _("Firmware")); var stack_switcher = new Gtk.StackSwitcher () { halign = Gtk.Align.CENTER, diff --git a/src/Views/FirmwareView.vala b/src/Views/FirmwareView.vala new file mode 100644 index 000000000..38e0e8284 --- /dev/null +++ b/src/Views/FirmwareView.vala @@ -0,0 +1,151 @@ +/* +* Copyright (c) 2020 elementary, Inc. (https://elementary.io) +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA +* +* Authored by: Marius Meisenzahl +*/ + +public class About.FirmwareView : Gtk.Stack { + private Gtk.Grid grid; + private Granite.Widgets.AlertView progress_alert_view; + private Gtk.Grid progress_view; + private Gtk.ListBox update_list; + private FwupdManager fwupd; + + construct { + fwupd = new FwupdManager (); + + transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT; + + progress_alert_view = new Granite.Widgets.AlertView ( + "", + _("Do not unplug the device during the update."), + "emblem-synchronized" + ); + progress_alert_view.get_style_context ().remove_class (Gtk.STYLE_CLASS_VIEW); + + progress_view = new Gtk.Grid () { + margin = 24 + }; + progress_view.attach (progress_alert_view, 0, 0); + + var no_devices_alert_view = new Granite.Widgets.AlertView ( + _("No Updatable Devices"), + _("Firmware updates are not supported on this or any connected devices."), + "application-x-firmware" + ); + no_devices_alert_view.show_all (); + no_devices_alert_view.get_style_context ().remove_class (Gtk.STYLE_CLASS_VIEW); + + update_list = new Gtk.ListBox () { + vexpand = true, + selection_mode = Gtk.SelectionMode.SINGLE + }; + update_list.set_placeholder (no_devices_alert_view); + + var scrolled_window = new Gtk.ScrolledWindow (null, null); + scrolled_window.add (update_list); + + var frame = new Gtk.Frame (null); + frame.add (scrolled_window); + + grid = new Gtk.Grid () { + column_spacing = 12, + row_spacing = 12, + margin = 12 + }; + grid.add (frame); + + add (grid); + add (progress_view); + + fwupd.on_device_added.connect (on_device_added); + fwupd.on_device_error.connect (on_device_error); + fwupd.on_device_removed.connect (on_device_removed); + + update_list_view.begin (); + } + + private async void update_list_view () { + foreach (unowned Gtk.Widget widget in update_list.get_children ()) { + if (widget is Widgets.FirmwareUpdateRow) { + update_list.remove (widget); + } + } + + foreach (var device in yield fwupd.get_devices ()) { + add_device (device); + } + + visible_child = grid; + update_list.show_all (); + } + + private void add_device (Fwupd.Device device) { + if (device.has_flag (Fwupd.DeviceFlag.UPDATABLE) && device.releases.length () > 0) { + var row = new Widgets.FirmwareUpdateRow (fwupd, device); + update_list.add (row); + + row.on_update_start.connect (() => { + progress_alert_view.title = _("“%s” is being updated".printf (device.name)); + visible_child = progress_view; + }); + row.on_update_end.connect (() => { + visible_child = grid; + update_list_view.begin (); + }); + } + } + + private void on_device_added (Fwupd.Device device) { + debug ("Added device: %s", device.name); + + add_device (device); + + visible_child = grid; + update_list.show_all (); + } + + private void on_device_error (Fwupd.Device device, string error) { + var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( + _("Failed to install firmware release"), + error, + device.icon, + Gtk.ButtonsType.CLOSE + ); + message_dialog.transient_for = (Gtk.Window) get_toplevel (); + message_dialog.badge_icon = new ThemedIcon ("dialog-error"); + message_dialog.show_all (); + message_dialog.run (); + message_dialog.destroy (); + } + + private void on_device_removed (Fwupd.Device device) { + debug ("Removed device: %s", device.name); + + foreach (unowned Gtk.Widget widget in update_list.get_children ()) { + if (widget is Widgets.FirmwareUpdateRow) { + var row = (Widgets.FirmwareUpdateRow) widget; + if (row.device.id == device.id) { + update_list.remove (widget); + } + } + } + + update_list.show_all (); + } +} diff --git a/src/Widgets/FirmwareUpdateRow.vala b/src/Widgets/FirmwareUpdateRow.vala new file mode 100644 index 000000000..bd1a65f70 --- /dev/null +++ b/src/Widgets/FirmwareUpdateRow.vala @@ -0,0 +1,185 @@ +/* +* Copyright 2020 elementary, Inc. (https://elementary.io) +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA +* +* Authored by: Marius Meisenzahl +*/ + +public class About.Widgets.FirmwareUpdateRow : Gtk.ListBoxRow { + public FwupdManager fwupd { get; construct set; } + public Fwupd.Device device { get; construct set; } + + public signal void on_update_start (); + public signal void on_update_end (); + + public FirmwareUpdateRow (FwupdManager fwupd, Fwupd.Device device) { + Object ( + fwupd: fwupd, + device: device + ); + } + + construct { + var icon = new Gtk.Image.from_icon_name (device.icon, Gtk.IconSize.DND) { + pixel_size = 32 + }; + + var device_name_label = new Gtk.Label (device.name) { + halign = Gtk.Align.START, + hexpand = true + }; + device_name_label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL); + + var version_label = new Gtk.Label (device.latest_release.version) { + wrap = true, + xalign = 0 + }; + + var grid = new Gtk.Grid () { + column_spacing = 12, + margin = 6 + }; + grid.attach (icon, 0, 0, 1, 2); + grid.attach (device_name_label, 1, 0); + grid.attach (version_label, 1, 1); + + switch (device.latest_release.flag) { + case Fwupd.ReleaseFlag.IS_UPGRADE: + if (device.latest_release.version == device.version) { + add_up_to_date_label (grid); + break; + } + + var update_button = new Gtk.Button.with_label (_("Update")) { + valign = Gtk.Align.CENTER + }; + update_button.clicked.connect (() => { + on_update_start (); + + update.begin (device, device.latest_release, (obj, res) => { + update.end (res); + on_update_end (); + }); + }); + grid.attach (update_button, 2, 0, 1, 2); + break; + default: + add_up_to_date_label (grid); + break; + } + + add (grid); + } + + private void add_up_to_date_label (Gtk.Grid grid) { + var update_to_date_label = new Gtk.Label (_("Up to date")) { + valign = Gtk.Align.CENTER + }; + grid.attach (update_to_date_label, 2, 0, 1, 2); + } + + private async void update (Fwupd.Device device, Fwupd.Release release) { + var path = yield fwupd.download_file (device, release.uri); + + var details = yield fwupd.get_release_details (device, path); + + if (details.caption != null) { + if (show_details_dialog (details) == false) { + return; + } + } + + if ((yield fwupd.install (device, path)) == true) { + if (device.has_flag (Fwupd.DeviceFlag.NEEDS_REBOOT)) { + show_reboot_dialog (); + } else if (device.has_flag (Fwupd.DeviceFlag.NEEDS_SHUTDOWN)) { + show_shutdown_dialog (); + } + } + } + + private bool show_details_dialog (Fwupd.Details details) { + var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( + _("“%s” needs to manually be put in update mode").printf (device.name), + details.caption, + device.icon, + Gtk.ButtonsType.CANCEL + ); + message_dialog.transient_for = (Gtk.Window) get_toplevel (); + + var suggested_button = new Gtk.Button.with_label (_("Continue")); + suggested_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + message_dialog.add_action_widget (suggested_button, Gtk.ResponseType.ACCEPT); + + if (details.image != null) { + var custom_widget = new Gtk.Image.from_file (details.image); + message_dialog.custom_bin.add (custom_widget); + } + + message_dialog.badge_icon = new ThemedIcon ("dialog-information"); + message_dialog.show_all (); + bool should_continue = message_dialog.run () == Gtk.ResponseType.ACCEPT; + + message_dialog.destroy (); + + return should_continue; + } + + private void show_reboot_dialog () { + var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( + _("An update requires the system to restart to complete"), + _("This will close all open applications and restart this device."), + "application-x-firmware", + Gtk.ButtonsType.CANCEL + ); + message_dialog.transient_for = (Gtk.Window) get_toplevel (); + + var suggested_button = new Gtk.Button.with_label (_("Restart")); + suggested_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + message_dialog.add_action_widget (suggested_button, Gtk.ResponseType.ACCEPT); + + message_dialog.badge_icon = new ThemedIcon ("system-reboot"); + message_dialog.show_all (); + if (message_dialog.run () == Gtk.ResponseType.ACCEPT) { + LoginManager.get_instance ().reboot (); + } + + message_dialog.destroy (); + } + + private void show_shutdown_dialog () { + var message_dialog = new Granite.MessageDialog.with_image_from_icon_name ( + _("An update requires the system to shut down to complete"), + _("This will close all open applications and turn off this device."), + "application-x-firmware", + Gtk.ButtonsType.CANCEL + ); + message_dialog.transient_for = (Gtk.Window) get_toplevel (); + + var suggested_button = new Gtk.Button.with_label (_("Shut Down")); + suggested_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + message_dialog.add_action_widget (suggested_button, Gtk.ResponseType.ACCEPT); + + message_dialog.badge_icon = new ThemedIcon ("system-shutdown"); + message_dialog.show_all (); + if (message_dialog.run () == Gtk.ResponseType.ACCEPT) { + LoginManager.get_instance ().shutdown (); + } + + message_dialog.destroy (); + } +} diff --git a/src/meson.build b/src/meson.build index bfead2376..2504d9cfd 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,8 +1,17 @@ plug_files = files( 'Plug.vala', + 'Interfaces/FwupdManager.vala', + 'Interfaces/LoginManager.vala', + 'Objects/Details.vala', + 'Objects/Device.vala', + 'Objects/DeviceFlag.vala', + 'Objects/Release.vala', + 'Objects/ReleaseFlag.vala', 'Utils/ARMPartDecoder.vala', + 'Views/FirmwareView.vala', 'Views/HardwareView.vala', - 'Views/OperatingSystemView.vala' + 'Views/OperatingSystemView.vala', + 'Widgets/FirmwareUpdateRow.vala' ) switchboard_dep = dependency('switchboard-2.0')