#!/usr/bin/python3
# -*- coding: utf-8 -*-

# system imports
import os
import sys
import gi
import time
import gettext
import io
import locale
import tarfile
import urllib.request
import subprocess
import pycurl
import datetime
import configparser
import traceback
import setproctitle
import platform
import re
import aptUpdater
import session_automatic_upgrades
import xapp.os

gi.require_version('Gtk', '3.0')
gi.require_version('Notify', '0.7')
from gi.repository import Gtk, Gdk, Gio, GLib, GObject, Notify, Pango
from xapp.GSettingsWidgets import *

# local imports
import logger
from preferences import PreferencesWindow

from Classes import MainloopTimer, PRIORITY_UPDATES, UpdateTracker
from util import on_battery, _idle, _async, Inhibitor


settings = Gio.Settings(schema_id="com.linuxmint.updates")
cinnamon_support = False
try:
    if settings.get_boolean("show-cinnamon-updates"):
        import cinnamon
        cinnamon_support = True
except Exception as e:
    if os.getenv("DEBUG"):
        print("No cinnamon update support:\n%s" % traceback.format_exc())

flatpak_support = False
try:
    if settings.get_boolean("show-flatpak-updates"):
        import flatpakUpdater
        flatpak_support = True
except Exception as e:
    if os.getenv("DEBUG"):
        print("No flatpak update support:\n%s" % traceback.format_exc())

CINNAMON_SUPPORT = cinnamon_support
FLATPAK_SUPPORT = flatpak_support

try:
    os.system("killall -q mintUpdate")
except Exception as e:
    print (e)
    print(sys.exc_info()[0])

setproctitle.setproctitle("mintUpdate")

# i18n
APP = 'mintupdate'
LOCALE_DIR = "/usr/share/locale"
locale.bindtextdomain(APP, LOCALE_DIR)
gettext.bindtextdomain(APP, LOCALE_DIR)
gettext.textdomain(APP)
_ = gettext.gettext


Notify.init(_("Update Manager"))

(TAB_DESC, TAB_PACKAGES, TAB_CHANGELOG) = range(3)

(UPDATE_CHECKED, UPDATE_DISPLAY_NAME, UPDATE_OLD_VERSION, UPDATE_NEW_VERSION, UPDATE_SOURCE, UPDATE_SIZE, UPDATE_SIZE_STR, UPDATE_TYPE_PIX, UPDATE_TYPE, UPDATE_TOOLTIP, UPDATE_SORT_STR, UPDATE_OBJ) = range(12)

MINUTE = 60
HOUR = 60 * MINUTE
DAY = 24 * HOUR

def size_to_string(size):
    f_size = float(size)
    units = [_('B'), _('KB'), _('MB'), _('GB')]
    for unit in units:
        if f_size >= 1000:
            f_size /= 1000
        else:
            break
    if f_size >= 10:
        return f"{f_size:.0f} {unit}"
    else:
        return f"{f_size:.1f} {unit}"


def name_search_func(model, column, key, iter):
    name = model.get_value(iter, column)
    return key.lower() not in name.lower() # False is a match


class APTCacheMonitor():
    """ Monitors package cache and dpkg status and runs the refresh thread() on change """

    def __init__(self, application):
        self.application = application
        self.cachetime = 0
        self.statustime = 0
        self.paused = True
        self.pkgcache = "/var/cache/apt/pkgcache.bin"
        self.dpkgstatus = "/var/lib/dpkg/status"
        self.start()

    @_async
    def start(self):
        self.update_cachetime()
        if os.path.isfile(self.pkgcache) and os.path.isfile(self.dpkgstatus):
            while True:
                if not self.paused and self.application.hidden:
                    try:
                        cachetime = os.path.getmtime(self.pkgcache)
                        statustime = os.path.getmtime(self.dpkgstatus)
                        if (cachetime != self.cachetime or statustime != self.statustime) and \
                            not self.application.dpkg_locked():
                            self.cachetime = cachetime
                            self.statustime = statustime
                            self.application.logger.write("Changes to the package cache detected; triggering refresh")
                            self.application.queue_refresh(False)
                    except Exception as e:
                        self.application.logger.write_error("APTCacheMonitor: %s" % e)
                time.sleep(90)
        else:
            self.application.logger.write("Package cache location not found, disabling cache monitoring")

    def resume(self, update_cachetime=True):
        if self.paused:
            if update_cachetime:
                self.update_cachetime()
            self.paused = False

    def pause(self):
        self.paused = True

    def update_cachetime(self):
        if os.path.isfile(self.pkgcache) and os.path.isfile(self.dpkgstatus):
            self.cachetime = os.path.getmtime(self.pkgcache)
            self.statustime = os.path.getmtime(self.dpkgstatus)

class MintUpdate():

    def __init__(self):
        self.information_window_showing = False
        self.history_window_showing = False
        self.preferences_window = None
        self.updates_inhibited = False
        self.reboot_required = False
        self.refreshing = False
        self.refresh_threads = []
        self.auto_refresh_source = MainloopTimer(self._auto_refresh_tick)
        self.session_update_source = MainloopTimer(self._session_update_tick)
        self.initial_refresh_done = False
        self.last_refresh_time = 0
        self.hidden = True # whether the window is hidden or not
        self.packages = [] # packages selected for update
        self.flatpaks = [] # flatpaks selected for update
        self.spices = [] # spices selected for update
        self.logger = logger.Logger()
        self.cache_monitor = APTCacheMonitor(self)
        self.logger.write("Launching Update Manager")
        self.test_mode = os.getenv("MINTUPDATE_TEST")
        self.settings = Gio.Settings(schema_id="com.linuxmint.updates")

        self.app_restart_required = False
        self.show_cinnamon_enabled = False
        self.column_upgrade = None
        self._bulk_toggle_paths = None
        self.settings.connect("changed", self._on_settings_changed)
        self._on_settings_changed(self.settings, None)

        #Set the Glade file
        gladefile = "/usr/share/linuxmint/mintupdate/main.ui"
        self.builder = Gtk.Builder()
        self.builder.set_translation_domain("mintupdate")
        self.builder.add_from_file(gladefile)

        #self.builder.connect_signals(self)
        for widget in self.builder.get_objects():
            if issubclass(type(widget), Gtk.Buildable):
                name = "ui_%s" % Gtk.Buildable.get_name(widget)
                if not "__" in name:
                    setattr(self, name, widget)

        self.context_id = self.ui_statusbar.get_context_id("mintUpdate")
        self.inhibitor = Inhibitor(self.logger, self.ui_window)
        self.ui_window.connect("key-press-event",self.on_key_press_event)
        self.treeview = self.builder.get_object("treeview_update")

        try:
            self.ui_window.set_title(_("Update Manager"))

            self.ui_window.set_icon_name("mintupdate")

            # Add mintupdate style class for easier theming
            self.ui_window.get_style_context().add_class('mintupdate')

            accel_group = Gtk.AccelGroup()
            self.ui_window.add_accel_group(accel_group)

            self.textview_packages = self.ui_textview_packages.get_buffer()
            self.textview_description = self.ui_textview_description.get_buffer()
            self.textview_changes = self.ui_textview_changes.get_buffer()

            # Welcome page
            self.ui_button_welcome_finish.connect("clicked", self.on_welcome_page_finished)
            self.ui_button_welcome_help.connect("clicked", self.open_help)

            # the treeview
            cr = Gtk.CellRendererToggle()
            cr.connect("toggled", self.toggled)
            cr.set_property("activatable", True)

            self.column_upgrade = Gtk.TreeViewColumn(_("Upgrade"), cr)
            self.column_upgrade.add_attribute(cr, "active", UPDATE_CHECKED)
            self.column_upgrade.set_sort_column_id(UPDATE_CHECKED)
            self.column_upgrade.set_resizable(True)

            column_name = Gtk.TreeViewColumn(_("Name"), Gtk.CellRendererText(), markup=UPDATE_DISPLAY_NAME)
            column_name.set_sort_column_id(UPDATE_DISPLAY_NAME)
            column_name.set_resizable(True)

            column_old_version = Gtk.TreeViewColumn(_("Old Version"), Gtk.CellRendererText(), text=UPDATE_OLD_VERSION)
            column_old_version.set_sort_column_id(UPDATE_OLD_VERSION)
            column_old_version.set_resizable(True)

            column_new_version = Gtk.TreeViewColumn(_("New Version"), Gtk.CellRendererText(), text=UPDATE_NEW_VERSION)
            column_new_version.set_sort_column_id(UPDATE_NEW_VERSION)
            column_new_version.set_resizable(True)

            column_size = Gtk.TreeViewColumn(_("Size"), Gtk.CellRendererText(), text=UPDATE_SIZE_STR)
            column_size.set_sort_column_id(UPDATE_SIZE)
            column_size.set_resizable(True)

            column_type = Gtk.TreeViewColumn(_("Type"), Gtk.CellRendererPixbuf(), icon_name=UPDATE_TYPE_PIX)
            column_type.set_sort_column_id(UPDATE_TYPE)
            column_type.set_resizable(True)

            column_origin = Gtk.TreeViewColumn(_("Origin"), Gtk.CellRendererText(), text=UPDATE_SOURCE)
            column_origin.set_sort_column_id(UPDATE_SOURCE)
            column_origin.set_resizable(True)

            self.treeview.set_tooltip_column(UPDATE_TOOLTIP)

            self.treeview.set_search_equal_func(name_search_func)
            self.treeview.append_column(column_type)
            self.treeview.append_column(self.column_upgrade)
            self.treeview.append_column(column_name)
            self.treeview.append_column(column_old_version)
            self.treeview.append_column(column_new_version)
            self.treeview.append_column(column_origin)
            self.treeview.append_column(column_size)

            self.treeview.set_headers_clickable(True)
            self.treeview.set_reorderable(False)
            self.treeview.show()

            self.treeview.connect("button-press-event", self.treeview_button_pressed)
            self.treeview.connect("button-release-event", self.treeview_right_clicked)
            self.treeview.connect("row-activated", self.treeview_row_activated)

            selection = self.treeview.get_selection()
            selection.set_mode(Gtk.SelectionMode.MULTIPLE)
            selection.connect("changed", self.display_selected_update)
            self.ui_notebook_details.connect("switch-page", self.switch_page)
            self.ui_window.connect("delete_event", self.close_window)

            # Install Updates button
            self.ui_install_button.connect("clicked", self.install)
            key, mod = Gtk.accelerator_parse("<Control>I")
            self.ui_install_button.add_accelerator("clicked", accel_group, key, mod, Gtk.AccelFlags.VISIBLE)

            # Clear button
            self.ui_clear_button.connect("clicked", self.clear)
            key, mod = Gtk.accelerator_parse("<Control><Shift>A")
            self.ui_clear_button.add_accelerator("clicked", accel_group, key, mod, Gtk.AccelFlags.VISIBLE)

            # Select All button
            self.ui_select_all_button.connect("clicked", self.select_all)
            key, mod = Gtk.accelerator_parse("<Control>A")
            self.ui_select_all_button.add_accelerator("clicked", accel_group, key, mod, Gtk.AccelFlags.VISIBLE)

            # Refresh button
            self.ui_refresh_button.connect("clicked", self.manual_refresh)
            key, mod = Gtk.accelerator_parse("<Control>R")
            self.ui_refresh_button.add_accelerator("clicked", accel_group, key, mod, Gtk.AccelFlags.VISIBLE)

            # Self-update page:
            self.ui_self_update_button.connect("clicked", self.self_update)

            # Tray icon menu
            menu = Gtk.Menu()
            image = Gtk.Image.new_from_icon_name("xsi-view-refresh-symbolic", Gtk.IconSize.MENU)
            menuItem3 = Gtk.ImageMenuItem(label=_("Refresh"), image=image)
            menuItem3.connect('activate', self.manual_refresh)
            menu.append(menuItem3)
            image = Gtk.Image.new_from_icon_name("xsi-dialog-information-symbolic", Gtk.IconSize.MENU)
            menuItem2 = Gtk.ImageMenuItem(label=_("Information"), image=image)
            menuItem2.connect('activate', self.open_information)
            menu.append(menuItem2)
            image = Gtk.Image.new_from_icon_name("xsi-toolbox-symbolic", Gtk.IconSize.MENU)
            menuItem4 = Gtk.ImageMenuItem(label=_("Preferences"), image=image)
            menuItem4.connect('activate', self.open_preferences)
            menu.append(menuItem4)
            image = Gtk.Image.new_from_icon_name("xsi-exit-symbolic", Gtk.IconSize.MENU)
            menuItem = Gtk.ImageMenuItem(label=_("Quit"), image=image)
            menuItem.connect('activate', self.quit)
            menu.append(menuItem)
            menu.show_all()

            self.statusIcon = XApp.StatusIcon()
            self.statusIcon.set_secondary_menu(menu)
            self.statusIcon.connect('activate', self.on_statusicon_activated)

            # Main window menu
            fileMenu = Gtk.MenuItem.new_with_mnemonic(_("_File"))
            fileSubmenu = Gtk.Menu()
            fileMenu.set_submenu(fileSubmenu)
            image = Gtk.Image.new_from_icon_name("xsi-window-close-symbolic", Gtk.IconSize.MENU)
            closeMenuItem = Gtk.ImageMenuItem(label=_("Close window"), image=image)
            closeMenuItem.connect("activate", self.hide_window)
            key, mod = Gtk.accelerator_parse("<Control>W")
            closeMenuItem.add_accelerator("activate", accel_group, key, mod, Gtk.AccelFlags.VISIBLE)
            fileSubmenu.append(closeMenuItem)
            fileSubmenu.append(Gtk.SeparatorMenuItem())
            image = Gtk.Image.new_from_icon_name("xsi-exit-symbolic", Gtk.IconSize.MENU)
            quitMenuItem = Gtk.ImageMenuItem(label=_("Quit"), image=image)
            quitMenuItem.connect('activate', self.quit)
            key, mod = Gtk.accelerator_parse("<Control>Q")
            quitMenuItem.add_accelerator("activate", accel_group, key, mod, Gtk.AccelFlags.VISIBLE)
            fileSubmenu.append(quitMenuItem)

            editMenu = Gtk.MenuItem.new_with_mnemonic(_("_Edit"))
            editSubmenu = Gtk.Menu()
            editMenu.set_submenu(editSubmenu)
            image = Gtk.Image.new_from_icon_name("xsi-toolbox-symbolic", Gtk.IconSize.MENU)
            prefsMenuItem = Gtk.ImageMenuItem(label=_("Preferences"), image=image)
            prefsMenuItem.connect("activate", self.open_preferences)
            editSubmenu.append(prefsMenuItem)
            if os.path.exists("/usr/bin/timeshift-gtk"):
                image = Gtk.Image.new_from_icon_name("xsi-document-open-recent-symbolic", Gtk.IconSize.MENU)
                sourcesMenuItem = Gtk.ImageMenuItem(label=_("System Snapshots"), image=image)
                sourcesMenuItem.connect("activate", self.open_timeshift)
                editSubmenu.append(sourcesMenuItem)
            if os.path.exists("/usr/bin/mintsources"):
                image = Gtk.Image.new_from_icon_name("xsi-software-install-symbolic", Gtk.IconSize.MENU)
                sourcesMenuItem = Gtk.ImageMenuItem(label=_("Software Sources"), image=image)
                sourcesMenuItem.connect("activate", self.open_repositories)
                editSubmenu.append(sourcesMenuItem)

            rel_edition = 'unknown'
            rel_codename = 'unknown'
            if os.path.exists("/etc/linuxmint/info"):
                with open("/etc/linuxmint/info", encoding="utf-8") as info:
                    for line in info:
                        line = line.strip()
                        if "EDITION=" in line:
                            rel_edition = line.split('=')[1].replace('"', '').split()[0]
                        if "CODENAME=" in line:
                            rel_codename = line.split('=')[1].replace('"', '').split()[0]

            rel_path = "/usr/share/mint-upgrade-info/%s/info" % rel_codename
            if os.path.exists(rel_path):
                config = configparser.ConfigParser()
                config.read(rel_path)
                if rel_edition.lower() in config['general']['editions']:
                    rel_target = config['general']['target_name']
                    image = Gtk.Image.new_from_icon_name("mintupdate-type-package-symbolic", Gtk.IconSize.MENU)
                    relUpgradeMenuItem = Gtk.ImageMenuItem(label=_("Upgrade to %s") % rel_target, image=image)
                    relUpgradeMenuItem.connect("activate", self.open_rel_upgrade)
                    editSubmenu.append(relUpgradeMenuItem)

            viewMenu = Gtk.MenuItem.new_with_mnemonic(_("_View"))
            viewSubmenu = Gtk.Menu()
            viewMenu.set_submenu(viewSubmenu)
            image = Gtk.Image.new_from_icon_name("xsi-document-open-recent-symbolic", Gtk.IconSize.MENU)
            historyMenuItem = Gtk.ImageMenuItem(label=_("History of Updates"), image=image )
            historyMenuItem.connect("activate", self.open_history)
            image = Gtk.Image.new_from_icon_name("xsi-dialog-information-symbolic", Gtk.IconSize.MENU)
            infoMenuItem = Gtk.ImageMenuItem(label=_("Information"), image=image)
            infoMenuItem.connect("activate", self.open_information)
            image = Gtk.Image.new_from_icon_name("xsi-dialog-information-symbolic", Gtk.IconSize.MENU)
            visibleColumnsMenuItem = Gtk.ImageMenuItem(label=_("Visible Columns"), image=image)
            visibleColumnsMenu = Gtk.Menu()
            visibleColumnsMenuItem.set_submenu(visibleColumnsMenu)

            typeColumnMenuItem = Gtk.CheckMenuItem(label=_("Type"))
            typeColumnMenuItem.set_active(self.settings.get_boolean("show-type-column"))
            column_type.set_visible(self.settings.get_boolean("show-type-column"))
            typeColumnMenuItem.connect("toggled", self.setVisibleColumn, column_type, "show-type-column")
            visibleColumnsMenu.append(typeColumnMenuItem)

            packageColumnMenuItem = Gtk.CheckMenuItem(label=_("Package"))
            packageColumnMenuItem.set_active(self.settings.get_boolean("show-package-column"))
            column_name.set_visible(self.settings.get_boolean("show-package-column"))
            packageColumnMenuItem.connect("toggled", self.setVisibleColumn, column_name, "show-package-column")
            visibleColumnsMenu.append(packageColumnMenuItem)

            oldVersionColumnMenuItem = Gtk.CheckMenuItem(label=_("Old Version"))
            oldVersionColumnMenuItem.set_active(self.settings.get_boolean("show-old-version-column"))
            column_old_version.set_visible(self.settings.get_boolean("show-old-version-column"))
            oldVersionColumnMenuItem.connect("toggled", self.setVisibleColumn, column_old_version, "show-old-version-column")
            visibleColumnsMenu.append(oldVersionColumnMenuItem)

            newVersionColumnMenuItem = Gtk.CheckMenuItem(label=_("New Version"))
            newVersionColumnMenuItem.set_active(self.settings.get_boolean("show-new-version-column"))
            column_new_version.set_visible(self.settings.get_boolean("show-new-version-column"))
            newVersionColumnMenuItem.connect("toggled", self.setVisibleColumn, column_new_version, "show-new-version-column")
            visibleColumnsMenu.append(newVersionColumnMenuItem)

            originColumnMenuItem = Gtk.CheckMenuItem(label=_("Origin"))
            originColumnMenuItem.set_active(self.settings.get_boolean("show-origin-column"))
            column_origin.set_visible(self.settings.get_boolean("show-origin-column"))
            originColumnMenuItem.connect("toggled", self.setVisibleColumn, column_origin, "show-origin-column")
            visibleColumnsMenu.append(originColumnMenuItem)

            sizeColumnMenuItem = Gtk.CheckMenuItem(label=_("Size"))
            sizeColumnMenuItem.set_active(self.settings.get_boolean("show-size-column"))
            column_size.set_visible(self.settings.get_boolean("show-size-column"))
            sizeColumnMenuItem.connect("toggled", self.setVisibleColumn, column_size, "show-size-column")
            visibleColumnsMenu.append(sizeColumnMenuItem)

            viewSubmenu.append(visibleColumnsMenuItem)

            descriptionsMenuItem = Gtk.CheckMenuItem(label=_("Show Descriptions"))
            descriptionsMenuItem.set_active(self.settings.get_boolean("show-descriptions"))
            descriptionsMenuItem.connect("toggled", self.setVisibleDescriptions)
            viewSubmenu.append(descriptionsMenuItem)

            viewSubmenu.append(historyMenuItem)

            viewSubmenu.append(infoMenuItem)
            helpMenu = Gtk.MenuItem.new_with_mnemonic(_("_Help"))
            helpSubmenu = Gtk.Menu()
            helpMenu.set_submenu(helpSubmenu)

            image = Gtk.Image.new_from_icon_name("xsi-security-high-symbolic", Gtk.IconSize.MENU)
            helpMenuItem = Gtk.ImageMenuItem(label=_("Welcome Screen"), image=image)
            helpMenuItem.connect("activate", self.show_welcome_page)
            helpSubmenu.append(helpMenuItem)
            if (Gtk.check_version(3,20,0) is None):
                image = Gtk.Image.new_from_icon_name("xsi-keyboard-shortcuts-symbolic", Gtk.IconSize.MENU)
                shortcutsMenuItem = Gtk.ImageMenuItem(label=_("Keyboard Shortcuts"), image=image)
                shortcutsMenuItem.connect("activate", self.open_shortcuts)
                helpSubmenu.append(shortcutsMenuItem)
            image = Gtk.Image.new_from_icon_name("xsi-help-contents-symbolic", Gtk.IconSize.MENU)
            helpMenuItem = Gtk.ImageMenuItem(label=_("Contents"), image=image)
            helpMenuItem.connect("activate", self.open_help)
            key, mod = Gtk.accelerator_parse("F1")
            helpMenuItem.add_accelerator("activate", accel_group, key, mod, Gtk.AccelFlags.VISIBLE)
            helpSubmenu.append(helpMenuItem)
            image = Gtk.Image.new_from_icon_name("xsi-help-about-symbolic", Gtk.IconSize.MENU)
            aboutMenuItem = Gtk.ImageMenuItem(label=_("About"), image=image)
            aboutMenuItem.connect("activate", self.open_about)
            helpSubmenu.append(aboutMenuItem)

            self.ui_menubar.append(fileMenu)
            self.ui_menubar.append(editMenu)
            self.ui_menubar.append(viewMenu)
            self.ui_menubar.append(helpMenu)

            self.ui_stack.set_visible_child_name("refresh_page")

            self.ui_window.resize(self.settings.get_int('window-width'), self.settings.get_int('window-height'))
            self.ui_paned.set_position(self.settings.get_int('window-pane-position'))

            self.ui_vbox.show_all()

            if len(sys.argv) > 1:
                showWindow = sys.argv[1]
                if showWindow == "show":
                    self.show_window()

            self.apt_updater = aptUpdater.AptUpdater(self.ui_window)

            if CINNAMON_SUPPORT:
                self.cinnamon_updater = cinnamon.UpdateManager()
            else:
                self.cinnamon_updater = None

            global FLATPAK_SUPPORT
            if FLATPAK_SUPPORT:
                try:
                    self.flatpak_updater = flatpakUpdater.FlatpakUpdater()
                except Exception as e:
                    print("Error creating FlatpakUpdater:", str(e))
                    self.flatpak_updater = None
                    FLATPAK_SUPPORT = False

            if self.settings.get_boolean("show-welcome-page"):
                self.show_welcome_page()
            else:
                self.queue_refresh(False)

            self.ui_notebook_details.set_current_page(0)

            self.start_auto_refresh()
            self.start_session_updates()

            Gtk.main()

        except Exception as e:
            print (e)
            print(sys.exc_info()[0])
            self.logger.write_error("Exception occurred in main thread: " + str(sys.exc_info()[0]))
            self.logger.close()

    def _on_settings_changed(self, settings, key, data=None):
        if key is None:
            self.show_flatpak_enabled = settings.get_boolean("show-flatpak-updates")
            self.show_cinnamon_enabled = settings.get_boolean("show-cinnamon-updates")
            return

        self.app_restart_required = settings.get_boolean("show-cinnamon-updates") != self.show_cinnamon_enabled or \
                                    settings.get_boolean("show-flatpak-updates") != self.show_flatpak_enabled

        if key in ("refresh-minutes", "refresh-hours", "refresh-days",
                   "autorefresh-minutes", "autorefresh-hours", "autorefresh-days",
                   "refresh-schedule-enabled"):
            self.start_auto_refresh()

        if key in ("auto-update-cinnamon-spices", "auto-update-flatpaks"):
            self.start_session_updates()


######### EVENT HANDLERS #########

    def on_key_press_event(self, widget, event):
        ctrl = (event.state & Gdk.ModifierType.CONTROL_MASK)
        if ctrl:
            if event.keyval == Gdk.KEY_s:
                self.select_updates(security=True)
            elif event.keyval == Gdk.KEY_k:
                self.select_updates(kernel=True)

######### UTILITY FUNCTIONS #########

    @_idle
    def set_status_message(self, message):
        self.ui_statusbar.push(self.context_id, message)

    @_idle
    def set_status(self, message, tooltip, icon, visible):
        self.set_status_message(message)
        self.statusIcon.set_icon_name(icon)
        self.statusIcon.set_tooltip_text(tooltip)
        self.statusIcon.set_visible(visible)

    @staticmethod
    def dpkg_locked():
        """ Returns True if a process has a handle on /var/lib/dpkg/lock (no check for write lock) """
        try:
            subprocess.run(["sudo", "/usr/lib/linuxmint/mintUpdate/dpkg_lock_check.sh"],
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
            return True
        except subprocess.CalledProcessError:
            return False

    @staticmethod
    def show_dpkg_lock_msg(parent):
        dialog = Gtk.MessageDialog(parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
        Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, _("Cannot Proceed"))
        dialog.format_secondary_markup(_("Another process is currently using the package management system. Please wait for it to finish and then try again."))
        dialog.set_title(_("Update Manager"))
        dialog.run()
        dialog.destroy()

    @_idle
    def show_infobar(self, title, msg, msg_type, icon, callback, ok_label=None):
        if not ok_label:
            ok_label = _("OK")

        infobar = Gtk.InfoBar()
        infobar.set_margin_bottom(2)
        infobar.set_message_type(msg_type)
        if not icon:
            if msg_type == Gtk.MessageType.WARNING:
                icon = "xsi-dialog-warning-symbolic"
            elif msg_type == Gtk.MessageType.ERROR:
                icon = "xsi-dialog-error-symbolic"
            elif msg_type == Gtk.MessageType.QUESTION:
                icon = "xsi-dialog-information-symbolic"
        if icon:
            img = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.LARGE_TOOLBAR)
        else:
            img = Gtk.Image.new_from_icon_name("xsi-dialog-warning-symbolic", Gtk.IconSize.LARGE_TOOLBAR)
        infobar.get_content_area().pack_start(img, False, False, 0)

        info_label = Gtk.Label()
        info_label.set_line_wrap(True)
        info_label.set_markup("<b>%s</b>\n%s" % (title, msg))
        infobar.get_content_area().pack_start(info_label, False, False, 0)
        if callback:
            if msg_type == Gtk.MessageType.QUESTION:
                infobar.add_button(_("Yes"), Gtk.ResponseType.YES)
                infobar.add_button(_("No"), Gtk.ResponseType.NO)
            else:
                infobar.add_button(ok_label, Gtk.ResponseType.OK)
            infobar.connect("response", callback)
        infobar.show_all()
        for child in self.ui_infobar.get_children():
            child.destroy()
        self.ui_infobar.pack_start(infobar, True, True, 0)

    @_idle
    def show_error(self, error_msg):
        self.ui_stack.set_visible_child_name("error_page")
        self.ui_label_error_details.set_text(error_msg)


    @_idle
    def show_updates_in_UI(self, num_visible, num_software, num_security, download_size, is_self_update, model_items):
        status_string = ""

        if num_visible > 0:
            self.logger.write("Found %d software updates" % num_visible)
            if is_self_update:
                self.ui_stack.set_visible_child_name("self_update_page")
                self.ui_statusbar.set_visible(False)
                details = []

                for item in model_items:
                    details.append(f"{item[0].source_name} {item[0].new_version}")
                details = ", ".join(details)
                self.ui_label_self_update_details.set_text(details)
            else:
                status_string = gettext.ngettext("%(selected)d update selected (%(size)s)",
                                        "%(selected)d updates selected (%(size)s)", num_visible) % \
                                        {'selected':num_visible, 'size':size_to_string(download_size)}
                self.ui_clear_button.set_sensitive(True)
                self.ui_select_all_button.set_sensitive(True)
                self.ui_install_button.set_sensitive(True)
                self.ui_window.set_sensitive(True)
            systray_tooltip = gettext.ngettext("%d update available", "%d updates available", num_visible) % num_visible

            if not self.reboot_required:
                self.set_status(status_string, systray_tooltip, "mintupdate-updates-available-symbolic", True)
        else:
            self.logger.write("System is up to date")
            self.ui_stack.set_visible_child_name("success_page")

            if not self.reboot_required:
                self.set_status("", _("Your system is up to date"), "mintupdate-up-to-date-symbolic",
                                not self.settings.get_boolean("hide-systray"))

        if self.reboot_required:
            self.set_status(status_string, _("Restart required"), "mintupdate-warning-symbolic", True)

        self.ui_notebook_details.set_current_page(0)

        tracker = UpdateTracker(self.settings, self.logger)
        model = Gtk.TreeStore(bool, str, str, str, str, GObject.TYPE_LONG, str, str, str, str, str, object)
        # UPDATE_CHECKED, UPDATE_DISPLAY_NAME, UPDATE_OLD_VERSION, UPDATE_NEW_VERSION, UPDATE_SOURCE,
        # UPDATE_SIZE, UPDATE_SIZE_STR, UPDATE_TYPE_PIX, UPDATE_TYPE, UPDATE_TOOLTIP, UPDATE_SORT_STR, UPDATE_OBJ

        model.set_sort_column_id(UPDATE_SORT_STR, Gtk.SortType.ASCENDING)
        for item in model_items:
            self.add_update_to_model(model, tracker, item)

        if tracker.active:
            if tracker.notify():
                self.show_tracker_notification(num_software, num_security)
            tracker.record()

        self.treeview.set_model(model)
        self.treeview.set_search_column(UPDATE_DISPLAY_NAME)

    def show_tracker_notification(self, num_software, num_security):
        security_msg = gettext.ngettext("%d security update", "%d security updates", num_security) % num_security
        software_msg = gettext.ngettext("%d software update", "%d software updates", num_software) % num_software
        msg = ""
        if num_security > 0:
            msg = "%s\n" % security_msg
        if num_software > 0:
            msg = "%s%s\n" % (msg, software_msg)
        msg = "%s\n%s" % (msg, _("Apply them to keep your operating system safe and up to date."))

        # We use self.notification (instead of just a variable) to keep a memory pointer
        # on the notification. Without doing this, the callbacks are never executed by Gtk/Notify.
        self.notification = Notify.Notification.new(_("Updates are available"), msg, "mintupdate-updates-available-symbolic")
        self.notification.set_urgency(2)
        self.notification.set_timeout(Notify.EXPIRES_NEVER)
        self.notification.add_action("show_updates", _("View updates"), self.on_notification_action, None)
        self.notification.add_action("enable_automatic_updates", _("Enable automatic updates"), self.on_notification_action, None)
        self.notification.show()

    def on_notification_action(self, notification, action_name, data):
        if action_name == "show_updates":
            os.system("/usr/lib/linuxmint/mintUpdate/mintUpdate.py show &")
        elif action_name == "enable_automatic_updates":
            self.open_preferences(None, show_automation=True)

######### WINDOW/STATUSICON ##########

    def close_window(self, window, event):
        self.save_window_size()
        self.hide_window(window)
        return True

    @_idle
    def hide_window(self, widget=None):
        self.ui_window.hide()
        self.hidden = True

    @_idle
    def show_window(self, time=Gtk.get_current_event_time()):
        self.ui_window.show()
        self.ui_window.present_with_time(time)
        self.hidden = False

    def set_window_busy(self, busy):
        if self.ui_window.get_window() is None:
            return

        if busy and not self.hidden:
            self.ui_window.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.WATCH))
            self.ui_window.set_sensitive(False)
        else:
            self.ui_window.get_window().set_cursor(None)
            self.ui_window.set_sensitive(True)

    @_idle
    def set_refresh_mode(self, enabled):
        if enabled:
            self.ui_refresh_spinner.start()
            self.ui_stack.set_visible_child_name("refresh_page")
            self.ui_toolbar.set_sensitive(False)
            self.ui_menubar.set_sensitive(False)
            self.ui_clear_button.set_sensitive(False)
            self.ui_select_all_button.set_sensitive(False)
            self.ui_install_button.set_sensitive(False)
        else:
            self.ui_refresh_spinner.stop()
            # Make sure we're never stuck on the status_refreshing page:
            if self.ui_stack.get_visible_child_name() == "refresh_page":
                self.ui_stack.set_visible_child_name("updates_page")
            self.ui_toolbar.set_sensitive(True)
            self.ui_menubar.set_sensitive(True)
        self.set_window_busy(enabled)


    def save_window_size(self):
        self.settings.set_int('window-width', self.ui_window.get_size()[0])
        self.settings.set_int('window-height', self.ui_window.get_size()[1])
        self.settings.set_int('window-pane-position', self.ui_paned.get_position())

######### MENU/TOOLBAR FUNCTIONS #########

    def update_installable_state(self):
        model = self.treeview.get_model()

        iter = model.get_iter_first()
        download_size = 0
        num_selected = 0
        while iter is not None:
            checked = model.get_value(iter, UPDATE_CHECKED)
            if checked:
                size = model.get_value(iter, UPDATE_SIZE)
                download_size = download_size + size
                num_selected = num_selected + 1
            iter = model.iter_next(iter)
        if num_selected == 0:
            self.ui_install_button.set_sensitive(False)
            self.set_status_message(_("No updates selected"))
        else:
            self.ui_install_button.set_sensitive(True)
            self.set_status_message(gettext.ngettext("%(selected)d update selected (%(size)s)", "%(selected)d updates selected (%(size)s)", num_selected) % {'selected':num_selected, 'size':size_to_string(download_size)})

    def setVisibleColumn(self, checkmenuitem, column, key):
        state = checkmenuitem.get_active()
        self.settings.set_boolean(key, state)
        column.set_visible(state)

    def setVisibleDescriptions(self, checkmenuitem):
        self.settings.set_boolean("show-descriptions", checkmenuitem.get_active())
        self.refresh(False)

    def clear(self, widget):
        model = self.treeview.get_model()
        if len(model):
            iter = model.get_iter_first()
            while iter is not None:
                model.set_value(iter, 0, False)
                iter = model.iter_next(iter)

        self.update_installable_state()

    def select_all(self, widget):
        self.select_updates()

    def select_updates(self, security=False, kernel=False):
        model = self.treeview.get_model()
        iter = model.get_iter_first()
        while iter is not None:
            update = model.get_value(iter, UPDATE_OBJ)
            if security:
                if update.type == "security":
                    model.set_value(iter, UPDATE_CHECKED, True)
            elif kernel:
                if update.type == "kernel":
                    model.set_value(iter, UPDATE_CHECKED, True)
            else:
                model.set_value(iter, UPDATE_CHECKED, True)
            iter = model.iter_next(iter)

        self.update_installable_state()

    def manual_refresh(self, widget):
        if self.dpkg_locked():
            self.show_dpkg_lock_msg(self.ui_window)
        else:
            self.refresh(True)

    def self_update(self, widget):
        self.select_all(widget)
        self.install(widget)

######### WELCOME PAGE FUNCTIONS #######

    def on_welcome_page_finished(self, button):
        self.settings.set_boolean("show-welcome-page", False)
        self.ui_toolbar.set_sensitive(True)
        self.ui_menubar.set_sensitive(True)
        self.updates_inhibited = False
        self.queue_refresh(False)

    def show_welcome_page(self, widget=None):
        self.updates_inhibited = True
        self.ui_stack.set_visible_child_name("welcome_page")
        self.set_status("", _("Welcome to the Update Manager"), "mintupdate-updates-available-symbolic", True)
        self.ui_toolbar.set_sensitive(False)
        self.ui_menubar.set_sensitive(False)

######### TREEVIEW/SELECTION FUNCTIONS #######

    def treeview_row_activated(self, treeview, path, view_column):
        self.toggled(None, path)

    def treeview_button_pressed(self, widget, event):
        if event.type != Gdk.EventType.BUTTON_PRESS:
            return False

        if event.button == Gdk.BUTTON_SECONDARY:
            path_info = widget.get_path_at_pos(int(event.x), int(event.y))
            if path_info is not None:
                sel_paths = widget.get_selection().get_selected_rows()[1]
                if any(p.compare(path_info[0]) == 0 for p in sel_paths):
                    return True
            return False

        if event.button != Gdk.BUTTON_PRIMARY:
            return False
        if event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK):
            return False
        path_info = widget.get_path_at_pos(int(event.x), int(event.y))
        if path_info is None:
            return False
        path, column, _x, _y = path_info
        if column is not self.column_upgrade:
            return False
        sel_paths = widget.get_selection().get_selected_rows()[1]
        if len(sel_paths) > 1 and any(p.compare(path) == 0 for p in sel_paths):
            self._bulk_toggle_paths = sel_paths
        return False

    def toggled(self, renderer, path):
        model = self.treeview.get_model()
        if isinstance(path, str):
            path = Gtk.TreePath.new_from_string(path)
        iter = model.get_iter(path)
        if iter is None:
            return

        new_value = not model.get_value(iter, UPDATE_CHECKED)

        bulk_paths = self._bulk_toggle_paths
        self._bulk_toggle_paths = None

        if bulk_paths is None:
            sel_paths = self.treeview.get_selection().get_selected_rows()[1]
            if len(sel_paths) > 1 and any(p.compare(path) == 0 for p in sel_paths):
                bulk_paths = sel_paths

        if bulk_paths is None:
            model.set_value(iter, UPDATE_CHECKED, new_value)
        else:
            for p in bulk_paths:
                it = model.get_iter(p)
                if it is not None:
                    model.set_value(it, UPDATE_CHECKED, new_value)
            GLib.idle_add(self._restore_selection, bulk_paths)

        self.update_installable_state()

    def _restore_selection(self, paths):
        sel = self.treeview.get_selection()
        sel.unselect_all()
        for p in paths:
            sel.select_path(p)
        return GLib.SOURCE_REMOVE

    @_idle
    def set_textview_changes_text(self, text):
        self.textview_changes.set_text(text)

    def display_selected_update(self, selection):
        try:
            self.textview_packages.set_text("")
            self.textview_description.set_text("")
            self.textview_changes.set_text("")
            model, paths = selection.get_selected_rows()
            if len(paths) > 1:
                self.display_multi_selection(model, paths)
                return
            if len(paths) == 1:
                iter = model.get_iter(paths[0])
                update = model.get_value(iter, UPDATE_OBJ)
                description = update.description.replace("\\n", "\n")
                desc_tab = self.ui_notebook_details.get_nth_page(TAB_DESC)

                if update.type == "cinnamon":
                    latest_change_str = _("Most recent change")
                    desc = "%s\n\n%s: %s" % (description, latest_change_str, update.commit_msg)

                    self.textview_description.set_text(desc)
                    self.ui_notebook_details.get_nth_page(TAB_PACKAGES).hide()
                    self.ui_notebook_details.get_nth_page(TAB_CHANGELOG).hide()
                    self.ui_notebook_details.set_current_page(TAB_DESC)
                elif update.type == "flatpak":
                    if update.link is not None and update.link != "":
                        website_label_str = _("Website: %s") % update.link
                        description = "%s\n\n%s" % (update.description, website_label_str)
                    else:
                        description = "%s" % update.description

                    self.textview_description.set_text(description)
                    self.ui_notebook_details.get_nth_page(TAB_PACKAGES).show()
                    self.ui_notebook_details.get_nth_page(TAB_CHANGELOG).hide()
                    self.ui_notebook_details.set_current_page(TAB_DESC)
                    self.display_package_list(update, is_flatpak=True)
                else:
                    self.textview_description.set_text(description)
                    self.ui_notebook_details.get_nth_page(TAB_PACKAGES).show()
                    self.ui_notebook_details.get_nth_page(TAB_CHANGELOG).show()
                    self.display_package_list(update)

                    if self.ui_notebook_details.get_current_page() == 2:
                        # Changelog tab
                        self.retrieve_changelog(update)
                        self.changelog_retriever_started = True
                    else:
                        self.changelog_retriever_started = False

        except Exception as e:
            print (e)
            print(sys.exc_info()[0])


    def get_ppa_info(self, origin):
        ppa_sources_file = "/etc/apt/sources.list"
        ppa_sources_dir = "/etc/apt/sources.list.d/"
        ppa_words = origin.lstrip("LP-PPA-").split("-")

        source = ppa_sources_file
        if os.path.exists(ppa_sources_dir):
            for filename in os.listdir(ppa_sources_dir):
                if filename.startswith(origin.lstrip("LP-PPA-")):
                    source = os.path.join(ppa_sources_dir, filename)
                    break
        if not os.path.exists(source):
            return None, None
        try:
            with open(source) as f:
                for line in f:
                    if (not line.startswith("#") and all(word in line for word in ppa_words)):
                        ppa_info = line.split("://")[1]
                        break
                else:
                    return None, None
        except EnvironmentError as e:
            print ("Error encountered while trying to get PPA owner and name: %s" % e)
            return None, None
        ppa_url, ppa_owner, ppa_name, ppa_x = ppa_info.split("/", 3)
        return ppa_owner, ppa_name

    def get_ppa_changelog(self, ppa_owner, ppa_name, source_package, version):
        max_tarball_size = 1000000
        print ("\nFetching changelog for PPA package %s/%s/%s ..." % (ppa_owner, ppa_name, source_package))
        if source_package.startswith("lib"):
            ppa_abbr = source_package[:4]
        else:
            ppa_abbr = source_package[0]
        deb_dsc_uri = "https://ppa.launchpadcontent.net/%s/%s/ubuntu/pool/main/%s/%s/%s_%s.dsc" % (ppa_owner, ppa_name, ppa_abbr, source_package, source_package, version)
        try:
            deb_dsc = urllib.request.urlopen(deb_dsc_uri, None, 10).read().decode("utf-8")
        except Exception as e:
            print ("Could not open Launchpad URL %s - %s" % (deb_dsc_uri, e))
            return
        for line in deb_dsc.split("\n"):
            if "debian.tar" not in line:
                continue
            tarball_line = line.strip().split(" ", 2)
            if len(tarball_line) == 3:
                deb_checksum, deb_size, deb_filename = tarball_line
                break
        else:
            deb_filename = None
        if not deb_filename or not deb_size or not deb_size.isdigit():
            print ("Unsupported debian .dsc file format. Skipping this package.")
            return
        if (int(deb_size) > max_tarball_size):
            print ("Tarball size %s B exceeds maximum download size %d B. Skipping download." % (deb_size, max_tarball_size))
            return
        deb_file_uri = "https://ppa.launchpadcontent.net/%s/%s/ubuntu/pool/main/%s/%s/%s" % (ppa_owner, ppa_name, ppa_abbr, source_package, deb_filename)
        try:
            deb_file = urllib.request.urlopen(deb_file_uri, None, 10).read().decode("utf-8")
        except Exception as e:
            print ("Could not download tarball from %s - %s" % (deb_file_uri, e))
            return
        if deb_filename.endswith(".xz"):
            cmd = ["xz", "--decompress"]
            try:
                xz = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
                xz.stdin.write(deb_file)
                xz.stdin.close()
                deb_file = xz.stdout.read()
                xz.stdout.close()
            except EnvironmentError as e:
                print ("Error encountered while decompressing xz file: %s" % e)
                return
        deb_file = io.BytesIO(deb_file)
        try:
            with tarfile.open(fileobj = deb_file) as f:
                deb_changelog = f.extractfile("debian/changelog").read()
        except tarfile.TarError as e:
            print ("Error encountered while reading tarball: %s" % e)
            return

        return deb_changelog


    @_async
    def retrieve_changelog(self, update):
        source_package = update.real_source_name
        origin = update.origin
        is_kernel_update = update.type == "kernel"

        # Remove the epoch if present in the version
        version = update.new_version
        if ":" in version:
            version = version.split(":")[-1]

        self.set_textview_changes_text(_("Downloading changelog..."))

        changelog = [_("No changelog available")]

        prefix = source_package[0]
        if (source_package.startswith("lib")):
            prefix = source_package[0:4]

        changelog_sources = []
        if origin == "linuxmint":
            changelog_sources.append(f"http://packages.linuxmint.com/dev/{source_package}_{version}_amd64.changes")
            changelog_sources.append(f"http://packages.linuxmint.com/dev/{source_package}_{version}_i386.changes")
        elif origin == "ubuntu":
            if is_kernel_update:
                # Ubuntu HWE kernel versions end with '~' followed by the Ubuntu version (e.g. ~22.04.1). This suffix needs to be removed to get the correct changelog URL
                kernel_version = version.split("~")[0]
                changelog_sources.append(f"https://changelogs.ubuntu.com/changelogs/pool/main/l/linux/linux_{kernel_version}/changelog")
            else:
                for component in ["main", "multiverse", "universe", "restricted"]:
                    changelog_sources.append(f"https://changelogs.ubuntu.com/changelogs/pool/{component}/{prefix}/{source_package}/{source_package}_{version}/changelog")
        elif origin == "debian":
            if is_kernel_update:
                changelog_sources.append(f"https://metadata.ftp-master.debian.org/changelogs/main/l/linux/linux_{version}_changelog")
            else:
                for component in ["main", "contrib", "non-free", "non-free-firmware"]:
                    changelog_sources.append(f"https://metadata.ftp-master.debian.org/changelogs/{component}/{prefix}/{source_package}/{source_package}_{version}_changelog")
        elif origin.startswith("LP-PPA"):
            ppa_owner, ppa_name = self.get_ppa_info(origin)
            if ppa_owner and ppa_name:
                deb_changelog = self.get_ppa_changelog(ppa_owner, ppa_name, source_package, version)
                if not deb_changelog:
                    changelog_sources.append(f"https://launchpad.net/~{ppa_owner}/+archive/ubuntu/{ppa_name}/+files/{source_package}_{version}_source.changes")
                else:
                    changelog = f"{deb_changelog}\n"
            else:
                print ("PPA owner or name could not be determined")

        for changelog_source in changelog_sources:
            try:
                print("Trying to fetch the changelog from: %s" % changelog_source)
                url = urllib.request.urlopen(changelog_source, None, 10)
                source = url.read().decode("utf-8")
                url.close()

                changelog = ""
                if "linuxmint.com" in changelog_source:
                    changes = source.split("\n")
                    for change in changes:
                        stripped_change = change.strip()
                        if stripped_change == ".":
                            change = ""
                        if change == "" or stripped_change.startswith("*") or stripped_change.startswith("["):
                            changelog = changelog + change + "\n"
                elif "launchpad.net" in changelog_source:
                    changes = source.split("Changes:")[1].split("Checksums")[0].split("\n")
                    for change in changes:
                        stripped_change = change.strip()
                        if stripped_change != "":
                            if stripped_change == ".":
                                stripped_change = ""
                            changelog = changelog + stripped_change + "\n"
                else:
                    changelog = source
                changelog = changelog.split("\n")
                break
            except:
                pass

        self.set_textview_changes_text("\n".join(changelog))

    def start_auto_refresh(self):
        # (Re)schedule the auto-refresh timer based on current settings and
        # last_refresh_time. Safe to call any time the schedule needs to be
        # reset — settings change, after a refresh kicks off, etc.
        self.auto_refresh_source.disarm()

        if not self.settings.get_boolean("refresh-schedule-enabled"):
            self.logger.write("Auto-refresh disabled in preferences")
            return

        configured_interval = self._compute_refresh_interval()
        if configured_interval == 0:
            # No interval configured; poll the setting again in a minute in case
            # it changes (settings change also re-triggers us via _on_settings_changed).
            interval = 60
        elif self.last_refresh_time:
            elapsed = int(time.time()) - self.last_refresh_time
            interval = max(60, configured_interval - elapsed)
        else:
            interval = configured_interval

        refresh_type = "Auto" if self.initial_refresh_done else "Initial"
        days = int(interval / DAY)
        hours = int((interval - days * DAY) / HOUR)
        minutes = int((interval - days * DAY - hours * HOUR) / MINUTE)
        self.logger.write(f"{refresh_type} refresh will happen in {days} day(s), {hours} hour(s) and {minutes} minute(s)")

        self.auto_refresh_source.arm(interval)

    def _auto_refresh_tick(self):
        if not self.settings.get_boolean("refresh-schedule-enabled"):
            return

        if not self.hidden or self.refreshing:
            why = "window is open" if not self.hidden else "another refresh is in progress"
            self.logger.write(f"Auto refresh deferred ({why}); will retry shortly")
            self.auto_refresh_source.arm(60)
            return

        self.logger.write("Update Manager is in tray mode; performing auto refresh")

        try:
            if not self.refresh(True):
                # refresh declined (e.g. updates_inhibited); try again shortly.
                self.auto_refresh_source.arm(60)
                return

            # self.refresh() will restart the timer.
        except Exception:
            self.logger.write_error(f"Exception in auto-refresh tick: {traceback.format_exc()}")
            self.auto_refresh_source.arm(60)

    def start_session_updates(self):
        # (Re)schedule the session-update timer based on current settings.
        # Safe to call any time the settings change.
        self.session_update_source.disarm()

        if not (self.settings.get_boolean("auto-update-cinnamon-spices") or
                self.settings.get_boolean("auto-update-flatpaks")):
            self.logger.write("Automatic Flatpak/Spice updates disabled in preferences")
            return

        # Arm shortly after the first APT refresh so we don't compete with it.
        if self.initial_refresh_done:
            self.session_update_source.arm(5 * MINUTE)
        else:
            self.session_update_source.arm(self._compute_refresh_interval() + 5 * MINUTE)

    def _session_update_tick(self):
        if not self.hidden or self.refreshing:
            why = "window is open" if not self.hidden else "another refresh is in progress"
            self.logger.write(f"Automatic Flatpak/Spice updates deferred ({why}); will retry shortly")
            self.session_update_source.arm(60)
            return

        if on_battery():
            self.logger.write("On battery power, deferring automatic Flatpak/Spice updates")
            self.session_update_source.arm(HOUR)
            return

        try:
            self.run_session_automatic_upgrades()
        except Exception:
            self.logger.write_error(f"Exception scheduling automatic Flatpak/Spice updates: {traceback.format_exc()}")
        self.session_update_source.arm(DAY)

    def _compute_refresh_interval(self):
        prefix = "auto" if self.initial_refresh_done else ""
        return (self.settings.get_int(f"{prefix}refresh-minutes") * MINUTE
                + self.settings.get_int(f"{prefix}refresh-hours") * HOUR
                + self.settings.get_int(f"{prefix}refresh-days") * DAY)

    def switch_page(self, notebook, page, page_num):
        model, paths = self.treeview.get_selection().get_selected_rows()
        if len(paths) == 1 and page_num == 2 and not self.changelog_retriever_started:
            # Changelog tab
            update = model.get_value(model.get_iter(paths[0]), UPDATE_OBJ)
            self.retrieve_changelog(update)
            self.changelog_retriever_started = True

    def display_package_list(self, update, is_flatpak=False):
        prefix = "\n    • "
        count = len(update.package_names)
        if is_flatpak:
            size_label = _("Total size: <")
            if update.installing:
                self.textview_packages.set_text("%s %s" % (size_label, size_to_string(update.size)))
                return
        else:
            size_label = _("Total size:")

        packages = "%s%s%s\n%s %s\n\n" % \
            (gettext.ngettext("This update affects the following installed package:",
                      "This update affects the following installed packages:",
                      count),
             prefix,
             prefix.join(sorted(update.package_names)),
             size_label, size_to_string(update.size))
        self.textview_packages.set_text(packages)

    def display_multi_selection(self, model, paths):
        self.textview_description.set_text(
            _("Select a single update to view its description and changelog."))

        package_names = set()
        total_size = 0
        for p in paths:
            update = model.get_value(model.get_iter(p), UPDATE_OBJ)
            package_names.update(update.package_names)
            total_size += update.size

        prefix = "\n    • "
        count = len(package_names)
        packages = "%s%s%s\n%s %s\n\n" % \
            (gettext.ngettext("The selected updates affect the following installed package:",
                      "The selected updates affect the following installed packages:",
                      count),
             prefix,
             prefix.join(sorted(package_names)),
             _("Total size:"), size_to_string(total_size))
        self.textview_packages.set_text(packages)

        self.ui_notebook_details.get_nth_page(TAB_PACKAGES).show()
        self.ui_notebook_details.get_nth_page(TAB_CHANGELOG).hide()
        self.changelog_retriever_started = False

    def treeview_right_clicked(self, widget, event):
        if event.button != Gdk.BUTTON_SECONDARY:
            return
        path_info = widget.get_path_at_pos(int(event.x), int(event.y))
        if path_info is None:
            return
        clicked_path = path_info[0]
        model = widget.get_model()
        selection = widget.get_selection()
        sel_paths = selection.get_selected_rows()[1]

        if any(p.compare(clicked_path) == 0 for p in sel_paths):
            target_paths = sel_paths
        else:
            target_paths = [clicked_path]
            selection.unselect_all()
            selection.select_path(clicked_path)

        source_packages = []
        seen = set()
        for p in target_paths:
            update = model.get_value(model.get_iter(p), UPDATE_OBJ)
            for pkg in update.source_packages:
                if pkg not in seen:
                    seen.add(pkg)
                    source_packages.append(pkg)

        count = len(target_paths)
        ignore_current = gettext.ngettext(
            "Ignore the current update for this package",
            "Ignore the current updates for these packages",
            count)
        ignore_future = gettext.ngettext(
            "Ignore all future updates for this package",
            "Ignore all future updates for these packages",
            count)

        menu = Gtk.Menu()
        menuItem = Gtk.MenuItem.new_with_mnemonic(ignore_current)
        menuItem.connect("activate", self.add_to_ignore_list, source_packages, True)
        menu.append(menuItem)
        menuItem = Gtk.MenuItem.new_with_mnemonic(ignore_future)
        menuItem.connect("activate", self.add_to_ignore_list, source_packages, False)
        menu.append(menuItem)
        menu.attach_to_widget(widget, None)
        menu.show_all()
        menu.popup(None, None, None, None, event.button, event.time)

    def add_to_ignore_list(self, widget, source_packages, versioned):
        blacklist = self.settings.get_strv("blacklisted-packages")
        for source_package in source_packages:
            if not versioned:
                source_package = source_package.split("=")[0]
            blacklist.append(source_package)
        self.settings.set_strv("blacklisted-packages", blacklist)
        self.refresh(False)

######### SYSTRAY #########

    def tray_activate(self, time=0):
        if self.ui_window.is_active():
            self.save_window_size()
            self.hide_window()
        else:
            self.show_window(time)

    def on_statusicon_activated(self, icon, button, time):
        if button == Gdk.BUTTON_PRIMARY:
            self.tray_activate(time)

    def quit(self, widget, data = None):
        if self.ui_window:
            self.hide_window()
        try:
            self.logger.write("Exiting - requested by user")
            self.logger.close()
            self.save_window_size()
        except:
            pass # cause log might already been closed
        # Whatever works best heh :)
        os.system("kill -9 %s &" % os.getpid())

######### INFORMATION SCREEN #########

    def open_information(self, widget):
        if self.information_window_showing:
            return
        def destroy_window(widget):
            self.logger.remove_callback()
            self.information_window_showing = False
            window.destroy()
        @_idle
        def update_log(line):
            textbuffer.insert(textbuffer.get_end_iter(), line)
        gladefile = "/usr/share/linuxmint/mintupdate/information.ui"
        builder = Gtk.Builder()
        builder.set_translation_domain("mintupdate")
        builder.add_from_file(gladefile)
        window = builder.get_object("main_window")
        window.set_title(_("Information"))
        window.set_icon_name("mintupdate")

        # Add mintupdate style class for easier theming
        self.ui_window.get_style_context().add_class('mintupdate')

        def copy_log_path(widget):
            clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
            clipboard.set_text(self.logger.log.name, -1)

        textbuffer = builder.get_object("log_textview").get_buffer()
        window.connect("destroy", destroy_window)
        builder.get_object("close_button").connect("clicked", destroy_window)
        builder.get_object("processid_label").set_text(str(os.getpid()))
        textbuffer.set_text(self.logger.read())
        builder.get_object("log_filename").set_text(str(self.logger.log.name))
        builder.get_object("copy_log_path_button").connect("clicked", copy_log_path)
        self.logger.set_callback(update_log)
        self.information_window_showing = True

######### HISTORY SCREEN #########

    def open_history(self, widget):
        if self.history_window_showing:
            return
        gladefile = "/usr/share/linuxmint/mintupdate/history.ui"
        builder = Gtk.Builder()
        builder.set_translation_domain("mintupdate")
        builder.add_from_file(gladefile)
        window = builder.get_object("main_window")
        window.set_icon_name("mintupdate")
        window.set_title(_("History of Updates"))

        # Add mintupdate style class for easier theming
        self.ui_window.get_style_context().add_class('mintupdate')

        (COL_DATE, COL_TYPE, COL_NAME, COL_OLD_VER, COL_NEW_VER) = range(5)
        model = Gtk.TreeStore(str, str, str, str, str)

        treeview = builder.get_object("treeview_history")
        column_date = Gtk.TreeViewColumn(_("Date"), Gtk.CellRendererText(), text=COL_DATE)
        column_date.set_sort_column_id(COL_DATE)
        column_date.set_resizable(True)
        column_type = Gtk.TreeViewColumn(_("Type"), Gtk.CellRendererText(), text=COL_TYPE)
        column_type.set_sort_column_id(COL_TYPE)
        column_type.set_resizable(True)
        column_package = Gtk.TreeViewColumn(_("Update"), Gtk.CellRendererText(), text=COL_NAME)
        column_package.set_sort_column_id(COL_NAME)
        column_package.set_resizable(True)
        self.column_old_version = Gtk.TreeViewColumn(_("Old Version"), Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END), text=COL_OLD_VER)
        self.column_old_version.set_sort_column_id(COL_OLD_VER)
        self.column_old_version.set_resizable(True)
        self.column_new_version = Gtk.TreeViewColumn(_("New Version"), Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END), text=COL_NEW_VER)
        self.column_new_version.set_sort_column_id(COL_NEW_VER)
        self.column_new_version.set_resizable(True)
        treeview.append_column(column_date)
        treeview.append_column(column_type)
        treeview.append_column(column_package)
        treeview.append_column(self.column_old_version)
        treeview.append_column(self.column_new_version)
        treeview.set_headers_clickable(True)
        treeview.set_reorderable(False)
        treeview.set_search_equal_func(name_search_func)
        treeview.set_enable_search(True)
        treeview.show()

        updates = []
        apt_updates = []
        cinnamon_updates = []
        flatpak_updates = []

        if os.path.isfile("/var/log/dpkg.log"):
            apt_updates = subprocess.run('zgrep " upgrade " -sh /var/log/dpkg.log*',
                stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)\
                .stdout.decode().split("\n")

            for pkg in apt_updates:
                values = pkg.split(" ")
                if len(values) == 6:
                    (date, time, action, package, oldVersion, newVersion) = values
                    if action != "upgrade" or oldVersion == newVersion:
                        continue
                    if ":" in package:
                        package = package.split(":")[0]

                    iter = model.insert_before(None, None)
                    model.set_value(iter, COL_NAME, package)
                    model.row_changed(model.get_path(iter), iter)
                    model.set_value(iter, COL_DATE, "%s - %s" % (date, time))
                    model.set_value(iter, COL_OLD_VER, oldVersion)
                    model.set_value(iter, COL_NEW_VER, newVersion)
                    model.set_value(iter, COL_TYPE, _("package"))

        if CINNAMON_SUPPORT:
            logfile = os.path.join(GLib.get_user_state_dir(), 'cinnamon', 'harvester.log')
            if os.path.isfile(logfile):
                cinnamon_updates += subprocess.run('grep " upgrade " -sh %s' % logfile,
                    stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True)\
                    .stdout.decode().split("\n")

                for pkg in cinnamon_updates:
                    values = pkg.split(" ")
                    if len(values) == 7:
                        (date, time, spice_type, action, package, oldVersion, newVersion) = values
                        if action != "upgrade" or oldVersion == newVersion:
                            continue
                        if ":" in package:
                            package = package.split(":")[0]

                        iter = model.insert_before(None, None)
                        model.set_value(iter, COL_NAME, package)
                        model.row_changed(model.get_path(iter), iter)
                        model.set_value(iter, COL_DATE, "%s - %s" % (date, time))
                        model.set_value(iter, COL_OLD_VER, oldVersion)
                        model.set_value(iter, COL_NEW_VER, newVersion)
                        model.set_value(iter, COL_TYPE, spice_type)

        if FLATPAK_SUPPORT:
            logfile = flatpakUpdater.LOG_PATH
            if os.path.isfile(logfile):
                with open(logfile, "r") as f:
                    for entry in f:
                        values = entry.strip("\n").split("::")
                        if len(values) == 7:
                            (date, time, fp_type, action, name, old_version, new_version) = values

                            iter = model.insert_before(None, None)
                            model.set_value(iter, COL_NAME, name)
                            model.row_changed(model.get_path(iter), iter)
                            model.set_value(iter, COL_DATE, "%s - %s" % (date, time))
                            model.set_value(iter, COL_OLD_VER, old_version)
                            model.set_value(iter, COL_NEW_VER, new_version)
                            model.set_value(iter, COL_TYPE, "flatpak-runtime" if fp_type == "runtime" else "flatpak-app")


        updates = apt_updates + cinnamon_updates + flatpak_updates

        model.set_sort_column_id(COL_DATE, Gtk.SortType.DESCENDING)
        treeview.set_model(model)
        treeview.set_search_column(COL_NAME)

        def on_query_tooltip(widget, x, y, keyboard, tooltip):
            if not widget.get_tooltip_context(x, y, keyboard):
                return False
            else:
                on_row, wx, wy, model, path, iter = widget.get_tooltip_context(x, y, keyboard)
                bx, by = widget.convert_widget_to_bin_window_coords(x, y)
                result = widget.get_path_at_pos(bx, by)

                if result is not None:
                    path, column, cx, cy = result
                    if column == self.column_old_version:
                        text = model[iter][COL_OLD_VER]
                    elif column == self.column_new_version:
                        text = model[iter][COL_NEW_VER]
                    else:
                        return False

                    tooltip.set_text(text)
                    return True

        treeview.connect("query-tooltip", on_query_tooltip)

        def destroy_window(widget):
            self.history_window_showing = False
            window.destroy()
        window.connect("destroy", destroy_window)
        builder.get_object("button_close").connect("clicked", destroy_window)
        self.history_window_showing = True

######### HELP/ABOUT/SHORTCUTS/SOURCES SCREEN #########

    def open_help(self, widget):
        os.system("xdg-open https://linuxmint-user-guide.readthedocs.io/en/latest/mintupdate.html &")

    def open_rel_upgrade(self, widget):
        os.system("/usr/bin/mint-release-upgrade &")

    def open_about(self, widget):
        dlg = Gtk.AboutDialog()
        dlg.set_transient_for(self.ui_window)
        dlg.set_title(_("About"))
        dlg.set_program_name("mintUpdate")
        dlg.set_comments(_("Update Manager"))
        try:
            h = open('/usr/share/common-licenses/GPL', encoding="utf-8")
            s = h.readlines()
            gpl = ""
            for line in s:
                gpl += line
            h.close()
            dlg.set_license(gpl)
        except Exception as e:
            print (e)
            print(sys.exc_info()[0])

        dlg.set_version("__DEB_VERSION__")
        dlg.set_icon_name("mintupdate")
        dlg.set_logo_icon_name("mintupdate")
        dlg.set_website("https://www.github.com/linuxmint/mintupdate")
        def close(w, res):
            if res == Gtk.ResponseType.CANCEL or res == Gtk.ResponseType.DELETE_EVENT:
                w.destroy()
        dlg.connect("response", close)
        dlg.show()

    def open_repositories(self, widget):
        self.ui_window.set_sensitive(False)
        self.run_mintsources()

    @_async
    def run_mintsources(self):
        proc = subprocess.Popen(["pkexec", "mintsources"])
        proc.wait()
        self.queue_refresh(False)

    def open_timeshift(self, widget):
        subprocess.Popen(["pkexec", "timeshift-gtk"])

    def open_shortcuts(self, widget):
        gladefile = "/usr/share/linuxmint/mintupdate/shortcuts.ui"
        builder = Gtk.Builder()
        builder.set_translation_domain("mintupdate")
        builder.add_from_file(gladefile)
        window = builder.get_object("shortcuts")
        window.connect("destroy", Gtk.Widget.destroyed, window)

        if self.ui_window != window.get_transient_for():
            window.set_transient_for(self.ui_window)

        window.show_all()
        window.present_with_time(Gtk.get_current_event_time())

######### PREFERENCES SCREEN #########

    def open_preferences(self, widget, show_automation=False):
        if self.preferences_window is not None:
            self.preferences_window.window.present()
            return
        self.ui_window.set_sensitive(False)
        self.preferences_window = PreferencesWindow(self, show_automation=show_automation)
        self.preferences_window.window.connect("destroy", self._on_preferences_destroyed)

    def _on_preferences_destroyed(self, window):
        self.ui_window.set_sensitive(True)
        self.preferences_window = None
        if self.app_restart_required:
            self.restart_app()
        else:
            self.refresh(False)

    def restart_app(self):
        self.logger.write("Restarting update manager...")
        os.system("/usr/lib/linuxmint/mintUpdate/mintUpdate.py show &")

######### REFRESH THREAD ##########

    def add_update_to_model(self, model, tracker, item):
        update, title, description, source, icon, sort_key, tooltip = item

        iter = model.insert_before(None, None)
        model.row_changed(model.get_path(iter), iter)

        model.set_value(iter, UPDATE_CHECKED, True)
        if self.settings.get_boolean("show-descriptions"):
            model.set_value(iter, UPDATE_DISPLAY_NAME, "<b>%s</b>\n%s" % (GLib.markup_escape_text(title),
                                                                          GLib.markup_escape_text(description)))
        else:
            model.set_value(iter, UPDATE_DISPLAY_NAME, "<b>%s</b>" % GLib.markup_escape_text(title))

        model.set_value(iter, UPDATE_OLD_VERSION, update.old_version)
        model.set_value(iter, UPDATE_NEW_VERSION, update.new_version)
        model.set_value(iter, UPDATE_SOURCE, source)
        model.set_value(iter, UPDATE_SIZE, update.size)
        model.set_value(iter, UPDATE_SIZE_STR, size_to_string(update.size))
        model.set_value(iter, UPDATE_TYPE_PIX, icon)
        model.set_value(iter, UPDATE_TYPE, update.type)
        model.set_value(iter, UPDATE_TOOLTIP, tooltip)
        model.set_value(iter, UPDATE_SORT_STR, sort_key)
        model.set_value(iter, UPDATE_OBJ, update)

        if tracker.active and update.type != "unstable":
            tracker.update(update)

    def check_policy(self):
        """ Check the presence of the Mint layer """
        p = subprocess.run(['apt-cache', 'policy'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={"LC_ALL": "C"})
        output = p.stdout.decode()
        if p.stderr:
            error_msg = p.stderr.decode().strip()
            self.logger.write_error("APT policy error:\n%s" % error_msg)
            return False
        mint_layer_found = False
        for line in output.split("\n"):
            line = line.strip()
            if line.startswith("700") and line.endswith("Packages") and "/upstream" in line:
                mint_layer_found = True
                break
        return mint_layer_found

    def mirror_check(self):
        """ Mirror-related notifications """
        infobar_message = None
        infobar_message_type = Gtk.MessageType.WARNING
        infobar_callback = self._on_infobar_mintsources_response
        try:
            if os.path.exists("/usr/bin/mintsources") and os.path.exists("/etc/apt/sources.list.d/official-package-repositories.list"):
                mirror_url = None
                with open("/etc/apt/sources.list.d/official-package-repositories.list", 'r') as sources_file:
                    for line in sources_file:
                        line = line.strip()
                        if line.startswith("deb ") and "main upstream import" in line:
                            mirror_url = line.split()[1]
                            if mirror_url.endswith("/"):
                                mirror_url = mirror_url[:-1]
                            break
                if mirror_url is None or not mirror_url.startswith("http"):
                    # The Mint mirror being used either cannot be found or is not an HTTP(s) mirror
                    pass
                elif mirror_url == "http://packages.linuxmint.com":
                    if not self.settings.get_boolean("default-repo-is-ok"):
                        infobar_title = _("Do you want to switch to a local mirror?")
                        infobar_message = _("Local mirrors are usually faster than packages.linuxmint.com.")
                        infobar_message_type = Gtk.MessageType.QUESTION
                elif not self.hidden:
                    # Only perform up-to-date checks when refreshing from the UI (keep the load lower on servers)
                    mint_timestamp = self.get_url_last_modified("http://packages.linuxmint.com/db/version")
                    mirror_timestamp = self.get_url_last_modified("%s/db/version" % mirror_url)
                    if mirror_timestamp is None:
                        if mint_timestamp is None:
                            # Both default repo and mirror are unreachable; so, assume there's no Internet connection
                            pass
                        else:
                            infobar_title = _("Please switch to another mirror")
                            infobar_message = _("%s is unreachable.") % mirror_url
                    elif mint_timestamp is not None:
                        mint_date = datetime.datetime.fromtimestamp(mint_timestamp)
                        now = datetime.datetime.now()
                        mint_age = (now - mint_date).days
                        if (mint_age > 2):
                            mirror_date = datetime.datetime.fromtimestamp(mirror_timestamp)
                            mirror_age = (mint_date - mirror_date).days
                            if (mirror_age > 2):
                                infobar_title = _("Please switch to another mirror")
                                infobar_message = gettext.ngettext("The last update on %(mirror)s was %(days)d day ago.",
                                                            "The last update on %(mirror)s was %(days)d days ago.",
                                                            (now - mirror_date).days) % \
                                                            {'mirror': mirror_url, 'days': (now - mirror_date).days}
        except:
            print(sys.exc_info()[0])
            # best effort, just print out the error
            print("An exception occurred while checking if the repositories were up to date: %s" % sys.exc_info()[0])

        if infobar_message:
            self.show_infobar(infobar_title,
                            infobar_message,
                            infobar_message_type,
                            None,
                            infobar_callback)

    def _on_infobar_mintsources_response(self, infobar, response_id):
        infobar.destroy()
        if response_id == Gtk.ResponseType.NO:
            self.settings.set_boolean("default-repo-is-ok", True)
        else:
            subprocess.Popen(["pkexec", "mintsources"])

    def get_url_last_modified(self, url):
        try:
            c = pycurl.Curl()
            c.setopt(pycurl.URL, url)
            c.setopt(pycurl.CONNECTTIMEOUT, 10)
            c.setopt(pycurl.TIMEOUT, 30)
            c.setopt(pycurl.FOLLOWLOCATION, 1)
            c.setopt(pycurl.NOBODY, 1)
            c.setopt(pycurl.OPT_FILETIME, 1)
            c.perform()
            filetime = c.getinfo(pycurl.INFO_FILETIME)
            if filetime < 0:
                return None
            else:
                return filetime
        except Exception as e:
            print (e)
            return None

    def checkDependencies(self, changes, cache):
        foundSomething = False
        for pkg in changes:
            for dep in pkg.candidateDependencies:
                for o in dep.or_dependencies:
                    try:
                        if cache[o.name].isUpgradable:
                            pkgFound = False
                            for pkg2 in changes:
                                if o.name == pkg2.name:
                                    pkgFound = True
                            if not pkgFound:
                                newPkg = cache[o.name]
                                changes.append(newPkg)
                                foundSomething = True
                    except Exception as e:
                        print (e)
                        pass # don't know why we get these..
        if (foundSomething):
            changes = self.checkDependencies(changes, cache)
        return changes

    def refresh_cleanup(self):
        # cleanup when finished refreshing
        self.refreshing = False
        self.inhibitor.uninhibit()
        self.cache_monitor.resume()
        self.set_refresh_mode(False)

    @_idle
    def queue_refresh(self, refresh_cache):
        self.refresh(refresh_cache)

    def refresh(self, refresh_cache):
        # Must be called on the main thread. Worker-thread callers should
        # use queue_refresh() instead.
        if self.refreshing:
            return False

        # This really only exists to show the window on first-run when show-welcome-page is True
        if self.updates_inhibited:
            self.logger.write("Updates are inhibited, skipping refresh")
            self.show_window()
            return False

        # Switch to status_refreshing page
        self.refreshing = True
        self.set_refresh_mode(True)
        self.set_status(_("Checking for updates"), _("Checking for updates"), "mintupdate-checking-symbolic", not self.settings.get_boolean("hide-systray"))
        self.inhibitor.inhibit("Checking for updates")
        self.cache_monitor.pause()

        if self.reboot_required:
            #Updated UI text from 'Reboot' to 'Restart' for consistency with Cinnamon.
            #Internal variables (like reboot_required) left unchanged for compatibility.
            self.show_infobar(_("Restart required"),
                _("You have installed updates that require a restart to take effect. Please restart your system as soon as possible."),
                Gtk.MessageType.WARNING,
                "xsi-reboot-symbolic",
                self._on_infobar_reboot,
                _("Restart"))

        self.refresh_threads = []

        if refresh_cache:
            # Note: All cache refresh happen asynchronously
            # refresh_updates() waits for them to finish

            self.last_refresh_time = int(time.time())
            self.initial_refresh_done = True

            self.refresh_threads.append(self.refresh_apt_cache())

            if CINNAMON_SUPPORT:
                self.refresh_threads.append(self.refresh_cinnamon_cache())

            if FLATPAK_SUPPORT:
                self.refresh_threads.append(self.refresh_flatpak_cache())

            self.start_auto_refresh()

        self.refresh_updates()
        return True

    def _on_infobar_reboot(self, parent, response_id):
        session = os.environ.get("XDG_CURRENT_DESKTOP")
        # Trigger reboot based on DE
        if session in ["Cinnamon", "X-Cinnamon"]:
            subprocess.run(['/usr/bin/cinnamon-session-quit', '--reboot'])
        elif session == 'MATE':
            subprocess.run(['/usr/bin/dbus-send', '--dest=org.gnome.SessionManager', '--type=method_call',
                '/org/gnome/SessionManager', 'org.gnome.SessionManager.RequestReboot'])
        elif session == 'XFCE':
            subprocess.run(['/usr/bin/xfce4-session-logout', '--reboot'])
        else:
            subprocess.run(['/usr/bin/systemctl', 'reboot'])

    @_async
    def refresh_apt_cache(self):
        self.logger.write("Refreshing APT cache")
        error = self.apt_updater.refresh(interactive=not self.hidden)
        if error is not None:
            self.logger.write_error(f"APT cache refresh did not complete successfully: {error}")

    @_async
    def refresh_cinnamon_cache(self):
        self.logger.write("Refreshing cache for Cinnamon updates")
        for spice_type in cinnamon.updates.SPICE_TYPES:
            try:
                self.cinnamon_updater.refresh_cache_for_type(spice_type)
            except:
                self.logger.write_error("Something went wrong fetching Cinnamon %ss: %s" % (spice_type, str(sys.exc_info()[0])))
                print("-- Exception occurred fetching Cinnamon %ss:\n%s" % (spice_type, traceback.format_exc()))

    @_async
    def refresh_flatpak_cache(self):
        self.logger.write("Refreshing cache for Flatpak updates")
        self.flatpak_updater.refresh()

    @_async
    def run_session_automatic_upgrades(self):
        self.logger.write("Running automatic Flatpak/Spice updates")
        self.inhibitor.inhibit("Running automatic Flatpak/Spice updates")
        try:
            error = session_automatic_upgrades.run()
            if error:
                self.logger.write_error(error)
        except Exception:
            self.logger.write_error(f"Exception in automatic Flatpak/Spice updates: {traceback.format_exc()}")
        self.inhibitor.uninhibit()
        self.queue_refresh(False)

    @_async
    def refresh_updates(self):
        # Wait for all the caches to be refreshed
        for t in self.refresh_threads:
            t.join(timeout=300)
            if t.is_alive():
                self.logger.write_error(
                    f"Cache refresh thread {t.name} did not complete within 300s; "
                    "the update list may be stale")

        # Check presence of Mint layer
        if self.test_mode == "layer-error" or (not self.check_policy()):
            error_msg = "%s\n%s\n%s" % (_("Your APT configuration is corrupt."),
            _("Do not install or update anything - doing so could break your operating system!"),
            _("To switch to a different Linux Mint mirror and solve this problem, click OK."))
            self.show_error(error_msg)
            self.set_status(_("Could not refresh the list of updates"),
                    "%s%s%s" % (_("Could not refresh the list of updates"), "\n\n" if error_msg else "", error_msg),
                    "mintupdate-error-symbolic", True)
            self.show_infobar(_("Please switch to another Linux Mint mirror"),
                _("Your APT configuration is corrupt."), Gtk.MessageType.ERROR, None,
                self._on_infobar_mintsources_response)
            self.refresh_cleanup()
            return
        self.logger.write("Checking for updates")

        try:
            if self.test_mode:
                self.apt_updater.fetch_test_updates(self.test_mode)
            else:
                self.apt_updater.fetch_updates()

            error = self.apt_updater.error
            updates = self.apt_updater.update_list

            if error is not None:
                self.logger.write_error("Error in aptUpdater.py, could not refresh the list of updates")
                if "apt.cache.FetchFailedException" in error and " changed its " in error:
                    error += "\n\n%s" % _("Run 'apt update' in a terminal window to address this")
                self.show_error(error)
                self.set_status(_("Could not refresh the list of updates"),
                "%s%s%s" % (_("Could not refresh the list of updates"), "\n\n" if error else "", error),
                "mintupdate-error-symbolic", True)
                self.refresh_cleanup()
                return

        except:
            print("-- Exception occurred in the refresh thread:\n%s" % traceback.format_exc())
            self.logger.write_error("Exception occurred in the refresh thread: %s" % str(sys.exc_info()[0]))
            self.set_status(_("Could not refresh the list of updates"),
                                        _("Could not refresh the list of updates"), "mintupdate-error-symbolic", True)
            self.refresh_cleanup()
            return

        try:
            model_items = []

            # Look at the updates one by one
            num_visible = 0
            num_security = 0
            num_software = 0
            download_size = 0
            is_self_update = False

            if len(updates) > 0:
                for update in updates:
                    # Check if self-update is needed
                    if update.source_name in PRIORITY_UPDATES:
                        is_self_update = True

                    shortdesc = update.short_description
                    if len(shortdesc) > 100:
                        try:
                            shortdesc = shortdesc[:100]
                            # Remove the last word.. in case we chomped
                            # a word containing an &#234; character..
                            # if we ended up with &.. without the code and ; sign
                            # pango would fail to set the markup
                            words = shortdesc.split()
                            shortdesc = " ".join(words[:-1]) + "..."
                        except:
                            pass

                    origin = update.origin.replace("linuxmint", "Linux Mint").replace("ubuntu", "Ubuntu").replace("LP-PPA-", "PPA ").replace("debian", "Debian")

                    if update.type == "security":
                        sort_key = 1
                        tooltip = _("Security update")
                        num_security += 1
                    elif update.type == "kernel":
                        sort_key = 2
                        tooltip = _("Kernel update")
                        num_security += 1
                    elif update.type == "unstable":
                        sort_key = 7
                        tooltip = _("Unstable software. Only apply this update to help developers beta-test new software.")
                    else:
                        if origin in ["Ubuntu", "Debian", "Linux Mint", "Canonical"]:
                            sort_key = 3
                            tooltip = _("Software update")
                        else:
                            sort_key = 4
                            tooltip = "%s\n%s" % (_("3rd-party update"), origin)
                            update.type = "3rd-party"
                        num_software += 1

                    title = update.display_name
                    description = shortdesc
                    source = f"{origin} / {update.archive}"
                    icon = f"mintupdate-type-{update.type}-symbolic"
                    model_items.append((update, title, description, source, icon, f"{sort_key}{update.display_name}", tooltip))

                    num_visible += 1
                    download_size += update.size

            if not self.test_mode:
                if FLATPAK_SUPPORT and self.flatpak_updater and not is_self_update:
                    blacklist = self.settings.get_strv("blacklisted-packages")

                    self.flatpak_updater.fetch_updates()
                    if self.flatpak_updater.error is None:
                        for update in self.flatpak_updater.updates:
                            update.type = "flatpak"
                            if update.ref_name in blacklist or update.source_packages[0] in blacklist:
                                continue
                            if update.flatpak_type == "app":
                                tooltip = _("Flatpak application")
                            else:
                                tooltip = _("Flatpak runtime")

                            title = update.name
                            description = update.summary
                            source = update.origin
                            icon = "mintupdate-type-flatpak-symbolic"
                            model_items.append((update, title, description, source, icon, f"5{update.ref_name}", tooltip))

                            num_software += 1
                            num_visible += 1
                            download_size += update.size

                if CINNAMON_SUPPORT and not is_self_update:
                    blacklist = self.settings.get_strv("blacklisted-packages")

                    for update in self.cinnamon_updater.get_updates():
                        update.real_source_name = update.uuid
                        update.source_packages = ["%s=%s" % (update.uuid, update.new_version)]
                        update.package_names = []
                        update.type = "cinnamon"
                        if update.uuid in blacklist or update.source_packages[0] in blacklist:
                            continue
                        if update.spice_type == cinnamon.SPICE_TYPE_APPLET:
                            tooltip = _("Cinnamon applet")
                        elif update.spice_type == cinnamon.SPICE_TYPE_DESKLET:
                            tooltip = _("Cinnamon desklet")
                        elif update.spice_type == "action":
                            # The constant cinnamon.SPICE_TYPE_ACTION is new in Cinnamon 6.0
                            # use the value "action" instead here so this code can be
                            # backported.
                            tooltip = _("Nemo action")
                        elif update.spice_type == cinnamon.SPICE_TYPE_THEME:
                            tooltip = _("Cinnamon theme")
                        else:
                            tooltip = _("Cinnamon extension")

                        title = update.uuid
                        description = update.name
                        source = "Linux Mint / cinnamon"
                        icon = "cinnamon-symbolic"
                        model_items.append((update, title, description, source, icon, f"6{update.uuid}", tooltip))

                        num_software += 1
                        num_visible += 1
                        download_size += update.size

            # Updates found, update status message
            self.show_updates_in_UI(num_visible, num_software, num_security, download_size, is_self_update, model_items)

            if FLATPAK_SUPPORT and self.flatpak_updater.error is not None and not is_self_update:
                self.logger.write("Could not check for flatpak updates: %s" % self.flatpak_updater.error)
                msg = _("Error checking for flatpak updates: %s") % self.flatpak_updater.error
                self.set_status_message(msg)

            # Check whether to display the mirror infobar
            self.mirror_check()

            self.logger.write("Refresh finished")

        except:
            print("-- Exception occurred while showing updates:\n%s" % traceback.format_exc())
            self.logger.write_error("Exception occurred while showing updates: %s" % str(sys.exc_info()[0]))
            self.set_status(_("Could not refresh the list of updates"),
                                        _("Could not refresh the list of updates"), "mintupdate-error-symbolic", True)

        finally:
            self.refresh_cleanup()


############## INSTALLATION #########

    def show_cinnamon_error(self, title, message):
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self._show_cinnamon_error_cb, title, message)

    def _show_cinnamon_error_cb(self, title, message):
        dialog = Gtk.MessageDialog(transient_for=self.ui_window,
                                   destroy_with_parent=True,
                                   modal=True,
                                   message_type=Gtk.MessageType.ERROR,
                                   buttons=Gtk.ButtonsType.OK,
                                   use_markup=True,
                                   text=f"<big><b>{title}</b></big>")
        dialog.set_title(_("Update Manager"))

        message_label = Gtk.Label(label=message, lines=20, wrap=True, wrap_mode=Pango.WrapMode.WORD_CHAR, selectable=True)
        message_label.set_max_width_chars(60)
        message_label.show()
        dialog.get_message_area().pack_start(message_label, False, False, 0)

        dialog.connect("response", lambda x, y: dialog.destroy())

        dialog.show_all()
        dialog.present()

        return GLib.SOURCE_REMOVE

    def install(self, widget):
        if self.dpkg_locked():
            self.show_dpkg_lock_msg(self.ui_window)
            return

        self.set_window_busy(True)

        # Find list of packages to install
        install_needed = False
        self.packages = []
        self.spices = []
        self.flatpaks = []
        model = self.treeview.get_model()
        iter = model.get_iter_first()
        while iter is not None:
            if model.get_value(iter, UPDATE_CHECKED):
                install_needed = True
                update = model.get_value(iter, UPDATE_OBJ)
                if update.type == "cinnamon":
                    self.spices.append(update)
                    self.logger.write("Will install spice " + str(update.uuid))
                    iter = model.iter_next(iter)
                    continue
                elif update.type == "flatpak":
                    self.flatpaks.append(update)
                    self.logger.write("Will install flatpak " + str(update.ref_name))
                    iter = model.iter_next(iter)
                    continue
                if update.type == "kernel":
                    for pkg in update.package_names:
                        if "-image-" in pkg:
                            self.reboot_required = True
                if update.type == "security" and \
                   [True for pkg in update.package_names if "nvidia" in pkg]:
                   self.reboot_required = True
                for package in update.package_names:
                    self.packages.append(package)
                    self.logger.write("Will install " + str(package))
            iter = model.iter_next(iter)

        self.settings.set_int("install-last-run", int(time.time()))

        if not install_needed:
            self.set_window_busy(False)
            return

        self.cache_monitor.pause()
        self.inhibitor.inhibit("Installing updates")
        self.logger.write("Install requested by user")
        self._do_install()

    @_async
    def _do_install(self):
        refresh_needed = False
        try:
            if self.packages:
                self.set_status(_("Installing updates"), _("Installing updates"),
                                "mintupdate-installing-symbolic", True)
                self.logger.write("Ready to launch aptkit")
                self.apt_updater.install_packages(self.packages)

                if self.apt_updater.install_cancelled:
                    self.logger.write("Install cancelled")
                    self.reboot_required = False
                    self.set_status("", "", "mintupdate-updates-available-symbolic", True)
                    self._post_install_cleanup(refresh_needed=False)
                    return

                if self.apt_updater.install_error:
                    self.logger.write_error(f"APT install failed: {self.apt_updater.install_error}")
                    self.set_status(_("Could not install the security updates"),
                                    _("Could not install the security updates"),
                                    "mintupdate-error-symbolic", True)
                    self._post_install_cleanup(refresh_needed=False)
                    return

                self.logger.write("Install finished successfully")
                # override the monitor since there's a forced refresh later already
                self.cache_monitor.update_cachetime()
                refresh_needed = True

                if self.settings.get_boolean("hide-window-after-update"):
                    self.hide_window()

                if [pkg for pkg in PRIORITY_UPDATES if pkg in self.packages]:
                    # Skip _post_install_cleanup — restart_app spawns a new
                    # instance that will take over.
                    self.inhibitor.uninhibit()
                    self.logger.write("Mintupdate was updated, restarting it...")
                    self.logger.close()
                    self.restart_app()
                    return

            if self.flatpaks:
                self.flatpak_updater.prepare_start_updates(self.flatpaks)
                self.flatpak_updater.perform_updates()
                if self.flatpak_updater.error is not None:
                    self.logger.write_error("Flatpak update failed %s" % self.flatpak_updater.error)
                refresh_needed = True

            if self.spices:
                self.set_status(_("Updating Cinnamon Spices"), _("Updating Cinnamon Spices"),
                                "mintupdate-installing-symbolic", True)
                need_cinnamon_restart = False
                try:
                    for update in self.spices:
                        self.cinnamon_updater.upgrade(update)
                        try:
                            if self.cinnamon_updater.spice_is_enabled(update):
                                need_cinnamon_restart = True
                        except:
                            need_cinnamon_restart = True
                except Exception as e:
                    self.logger.write_error("Cinnamon spice install failed %s" % str(e))
                    self.show_cinnamon_error(_("Could not update Cinnamon Spices"), str(e))
                if need_cinnamon_restart and not self.reboot_required and \
                        os.getenv("XDG_CURRENT_DESKTOP") in ["Cinnamon", "X-Cinnamon"]:
                    subprocess.run(["cinnamon-dbus-command", "RestartCinnamon", "0"])
                refresh_needed = True
        except Exception as e:
            print(e)
            self.logger.write_error("Exception occurred in the install thread: " + str(sys.exc_info()[0]))

        self._post_install_cleanup(refresh_needed)

    def _post_install_cleanup(self, refresh_needed):
        self.inhibitor.uninhibit()
        self.cache_monitor.resume(False)
        GLib.idle_add(self.set_window_busy, False)
        if refresh_needed:
            self.queue_refresh(False)

if __name__ == "__main__":
    try:
        xapp.os.add_network_proxy_to_env()
    except Exception as e:
        print("Network proxy support unavailable: %s" % str(e), file=sys.stderr)

    MintUpdate()
