#!/usr/bin/env python
# -*- coding: utf-8 -*-

#############################################################################
##
## Copyright 2007-2009 Canonical Ltd
## Author: Jonathan Riddell <jriddell@ubuntu.com>
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License as
## published by the Free Software Foundation; either version 2 of 
## the License, or (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
#############################################################################

import sys, os, time

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4 import uic
from PyKDE4.kdecore import *
from PyKDE4.kdeui import *
from PyKDE4.kio import *

import dbus
import dbus.mainloop.qt
import dbus.service

import sys
sys.path.insert(0, '/usr/lib/update-notifier')
import apt_check

def translate(self, prop):
    """reimplement method from uic to change it to use gettext"""
    if prop.get("notr", None) == "true":
        return self._cstring(prop)
    else:
        if prop.text is None:
            return ""
        text = prop.text.encode("UTF-8")
        return i18n(text)

uic.properties.Properties._string = translate

from UpdateManager.Core.MetaRelease import MetaReleaseCore
from UpdateManager.DistUpgradeFetcherKDE import DistUpgradeFetcherKDE
import time

MULTIMEDIA_CATEGORY = "multimedia"
SEMANTIC_DESKTOP_CATEGORY = "nepomuk"

class App(QObject):
    """an applet to show a systray icon when apt has software updates to be installed, when Apport has crash reports, for reboot notification and for upgrade hook messages"""
    def __init__(self, distUpgrade=False, useDevelopmentRelease=False, useProposed=False, checkNewPackages=False):
        QObject.__init__(self)
        self.checkNewPackages = checkNewPackages #new package checker now replaced by kpackagekit
        if distUpgrade or useDevelopmentRelease or useProposed:
            self.upgradeMode = True
            self.newReleaseCheck(useDevelopmentRelease, useProposed)
        else:
            self.upgradeMode = False
            self.init()
        dbusInterface = DBusInterface(self)

    def init(self):
        self.tray = KSystemTrayIcon(KIcon("system-software-update"))
        self.connect(self.tray, SIGNAL("activated( QSystemTrayIcon::ActivationReason )"), self.activated)
        self.dirWatch = KDirWatch(self)
        self.dirWatch.addDir("/var/lib/apt/lists/") #apt-get update has been run
        self.dirWatch.addFile("/var/lib/update-notifier/dpkg-run-stamp") #something has been installed
        self.connect(self.dirWatch, SIGNAL("dirty(const QString &)"), self.aptDirectoryChanged)
        QTimer.singleShot(5000, self.aptDirectoryChanged) #run check shortly after startup

        try:
            pipe = os.popen("lsb_release --id -s")
            self.distroName = pipe.read().strip()
            del pipe
            if self.distroName == "Ubuntu":
                self.distroName = "Kubuntu"
        except Exception, e:
            self.distroName = i18nc("fallback distro name", "distribution release")

        self.rebootTray = KSystemTrayIcon(KIcon("system-reboot"))
        self.connect(self.rebootTray, SIGNAL("activated( QSystemTrayIcon::ActivationReason )"), self.rebootActivated)
        menu = self.rebootTray.contextMenu()
        menu.addAction(i18n("Do not show again"), self.hideReboot)

        self.apport_executable = self.findApport()
        if self.apport_executable is not None:
            self.apportWatch = KDirWatch(self)
            self.apportWatch.addDir("/var/crash/")
            self.connect(self.apportWatch, SIGNAL("dirty(const QString &)"), self.runApport)
            self.showApportIcon()

        self.hooksTray = KSystemTrayIcon(KIcon("help-hint"))
        self.hooksWatch = KDirWatch(self)
        self.hooksWatch.addDir("/var/lib/update-notifier/user.d/")
        self.connect(self.hooksWatch, SIGNAL("dirty(const QString &)"), self.processUpgradeHooks)
        self.processUpgradeHooks()

        self.codecsTray = KSystemTrayIcon(KIcon("download"))
        self.connect(self.codecsTray, SIGNAL("activated( QSystemTrayIcon::ActivationReason )"), self.codecsActivated)
        menu = self.codecsTray.contextMenu()
        menu.addAction(i18n("Do not show again"), self.hideCodecs)

        #the restricted packages we want to install, a description, a file to check if they are already installed and a category
        self.packages = {'flashplugin-installer': [i18n('Flash'), MULTIMEDIA_CATEGORY],
                    'libxine1-ffmpeg': [i18n('MPEG Plugins'), MULTIMEDIA_CATEGORY],
                    'libavcodec-unstripped-52': [i18n('Video Codecs'), MULTIMEDIA_CATEGORY],
                    'libdvdread4': [i18n('DVD Reading'), MULTIMEDIA_CATEGORY],
                    'libk3b6-extracodecs': [i18n('K3B CD Codecs'), MULTIMEDIA_CATEGORY],
                    'libmp3lame0': [i18n('MP3 Encoding'), MULTIMEDIA_CATEGORY],
                    'libtunepimp5-mp3': [i18n('MP3 Tag Reading'), MULTIMEDIA_CATEGORY],
                    'soprano-backend-sesame': [i18n('Improved Semantic Desktop Backend'), SEMANTIC_DESKTOP_CATEGORY],
                    }

    def newReleaseCheck(self, useDevelopmentRelease, useProposed):
        """ check for updates when user asks for --devel-release or --proposed, if there are any show the wizard """
        metaRelease = MetaReleaseCore(useDevelopmentRelease, useProposed)
        while metaRelease.downloading:
            time.sleep(1)
        self.new_dist = metaRelease.new_dist
        self.trayToolTip = ""
        showTray = False
        if self.new_dist is not None:
            self.fetcher = DistUpgradeFetcherKDE(useDevelopmentRelease, useProposed) #runs sys.exit() if no result
            QTimer.singleShot(10, self.runDistUpgrade)
        else:
            KMessageBox.sorry(None, i18n("No new upgrade available"), i18n("No Upgrade"))
            sys.exit()

    def runDistUpgrade(self):
        result = self.fetcher.run()
        if self.upgradeMode == True and (result is None or result == False):
            sys.exit()

    #update checking
    def aptDirectoryChanged(self, path=None):
        """ check for updates, if there are any show the tray icon """
        metaRelease = MetaReleaseCore()
        while metaRelease.downloading:
            time.sleep(1)
        self.new_dist = metaRelease.new_dist
        # a previous run may have enabled tooltip. reset if not visible only
        if not self.tray.isVisible():
            self.trayToolTip = ""
        showTray = False
        if self.new_dist is not None:
            self.trayToolTip = i18nc("distro name, version number", "An upgrade to %1 %2 is available.", self.distroName, self.new_dist.version)
            showTray = True
            menu = self.tray.contextMenu()
            if not self.tray.isVisible():
                upgradeAction = menu.addAction(i18nc("distro name, version number", "Upgrade to %1 %2", self.distroName, self.new_dist.version))
                self.connect(upgradeAction, SIGNAL("triggered(bool)"), self.upgradeActionTriggered)

        # check if a reboot is needed
        self.checkRebootRequired(self)

        if self.checkNewPackages:
            result = apt_check.run()
            if result[1] > 0:
                #security updates available, show different icon?
                pass
            if result[0] > 0:
                showTray = True
                if not self.tray.isVisible():
                    self.updates = result[0]
                    self.trayToolTip = i18np("A software update is available", "%1 software updates are available", self.updates)
                    if self.new_dist is not None:
                        self.trayToolTip = i18np("A software update is available.\nAn upgrade to %2 %3 is also available.", "%1 software updates are available.\nAn upgrade to %2 %3 is also available.", self.updates, self.distroName, self.new_dist.version)
        self.tray.setToolTip(self.trayToolTip)
        self.tray.setVisible(showTray)

    def showMessage(self):
        KNotification.event("Updates", self.trayToolTip, KIcon("system-software-update").pixmap(QSize(22,22)))

    def upgradeActionTriggered(self, checked):
        """called from tray icon right click menu"""
        self.fetcher = DistUpgradeFetcherKDE()
        QTimer.singleShot(10, self.runDistUpgrade)

    def activated(self, activationReason):
        """run adept when icon is clicked on"""
        if activationReason == QSystemTrayIcon.Trigger:
            doingDistUpgrade = False
            if self.new_dist is not None:
                distUpgrade = KMessageBox.questionYesNo(None, i18n("Do you want to do a full upgrade to the new distribution release %1?", self.new_dist.version), i18n("Full Upgrade?"), KStandardGuiItem.yes(), KStandardGuiItem.no(), "distUpgrade")
                if distUpgrade == KMessageBox.Yes:
                    doingDistUpgrade = True
                    self.fetcher = DistUpgradeFetcherKDE()
                    QTimer.singleShot(10, self.runDistUpgrade)
            if not doingDistUpgrade:
                KProcess.startDetached("kcmshell4", ["kpk_update"])

    #reboot notification
    def checkRebootRequired(self, path=None):
        config = KGlobal.config()
        configGroup = config.group("updateNotifications")
        dontShow = configGroup.readEntry("hideRebootNotifier", QVariant(False))
        if QFile.exists("/var/run/reboot-required") and QFile.exists("/var/lib/update-notifier/dpkg-run-stamp") and not self.rebootTray.isVisible() and not dontShow.toBool():
            self.rebootTray.show()
            self.rebootTray.setToolTip(i18n("Newly updated software needs your system to be rebooted before it can be used."))

    def hideReboot(self):
        """called by right click menu"""
        config = KGlobal.config()
        configGroup = config.group("updateNotifications")
        configGroup.writeEntry("hideRebootNotifier", QVariant(True))
        config.sync()
        self.rebootTray.hide()

    def showRebootMessage(self):
        KNotification.event("Reboot", i18n("Newly updated software needs your system to be rebooted before it can be used."), KIcon("system-reboot").pixmap(QSize(22,22)))

    def rebootActivated(self, activationReason):
        if activationReason == QSystemTrayIcon.Trigger:
            KProcess.startDetached("qdbus", ["org.kde.ksmserver", "/KSMServer", "org.kde.KSMServerInterface.logout", "1", "1", "3"]) #ShutdownConfirmYes ShutdownTypeReboot ShutdownModeInteractive - see README.kworkspace for other possibilities

    # Apport
    def findApport(self):
        """
        If apport is enabled and present, return the path to the
        executable to use.  Otherwise, return None.
        """
        apport_executable = None
        if QFile.exists("/etc/default/apport"):
            apportconfigfile = open("/etc/default/apport")
            for line in apportconfigfile:
                if line.find("enabled=1") >= 0:
                    if QFile.exists("/usr/share/apport/apport-kde"):
                        apport_executable = "/usr/share/apport/apport-kde"
                    elif QFile.exists("/usr/share/apport/apport-gtk"):
                        apport_executable = "/usr/share/apport/apport-gtk"
                    break
            apportconfigfile.close()
        return apport_executable

    def runApport(self):
        time.sleep(3) #sleep briefly, else we are too fast for apport
        result = os.system("/usr/share/apport/apport-checkreports --system")
        if result == 0:
            KProcess.startDetached("kdesudo", [self.apport_executable])
        else:
            KProcess.startDetached(self.apport_executable)

        # make sure we hide something that exists
        if self.apportTray is not None:
            self.apportTray.hide()

    def showApportIcon(self):
        """ run on startup, show apport icon if crashes are outstanding """
        result = os.system("/usr/share/apport/apport-checkreports")
        if result == 0:
            self.apportTray = KSystemTrayIcon(KIcon("apport"))
            self.connect(self.apportTray, SIGNAL("activated( QSystemTrayIcon::ActivationReason )"), self.apportActivated)
            self.apportTray.show()
            self.crashMessage = i18n("An application has crashed on your "
                                     "system (now or in the past).\n"
                                     "Click to "
                                     "display details. ")
            self.apportTray.setToolTip(self.crashMessage)
        else:
            # prevent AttributeError exception if apportTray was not created above
            self.apportTray = None

    def showApportMessage(self):
        KNotification.event("Crash", self.crashMessage, KIcon("apport").pixmap(QSize(22,22)))

    def apportActivated(self, activationReason):
        if activationReason == QSystemTrayIcon.Trigger:
            self.runApport()

    #upgrade hooks, see https://wiki.kubuntu.org/InteractiveUpgradeHooks
    def processUpgradeHooks(self):
        """show systray icon if upgrade hook messages need to be shown"""
        files = os.listdir("/var/lib/update-notifier/user.d/")
        self.fileInfos = {}
        for file in files:
            fileResult = self.processUpgradeHook(file)
            if fileResult != None:
                self.fileInfos[file] = fileResult
                self.fileInfos[file]["fileName"] = file
        if len(self.fileInfos) > 0:
            self.connect(self.hooksTray, SIGNAL("activated( QSystemTrayIcon::ActivationReason )"), self.hooksActivated)
            self.hooksTray.setToolTip(i18n("Software upgrade notifications are available"))
            self.hooksTray.show()

    def processUpgradeHook(self, file):
        """parse an upgrade hook file and return it if it should be shown"""
        fileHandle = open("/var/lib/update-notifier/user.d/" + file)
        fileInfo = {}
        for line in fileHandle:
            colon = line.find(":")
            space = line.find(" ")
            if (colon > 0 and space == -1) or (colon != -1 and colon < space):
                list = line.split(":", 1)
                key = list[0]
                fileInfo[key] = list[1].strip()
            else:
                fileInfo[key] += line

        #check if seen already
        config = KGlobal.config()
        configGroup = config.group("updateNotifications")
        alreadySeen = configGroup.readEntry(file, QVariant(False))
        if alreadySeen.toBool():
            return

        #check if before a reboot
        if "DontShowAfterReboot" in fileInfo and fileInfo["DontShowAfterReboot"] == "True":
            uptimeFile = open("/proc/uptime")
            uptimeLine = uptimeFile.readline()
            uptime = float(uptimeLine.split(" ")[0])
            now = time.time()
            stat = os.stat("/var/lib/update-notifier/user.d/" + file)
            #if((int)uptime > 0 && (now - mtime) > (int)uptime) { then skip
            if uptime > 0 and ((now - stat.st_mtime) > uptime):
                return
        #check if DisplayIf true
        if "DisplayIf" in fileInfo:
            display = os.system(fileInfo["DisplayIf"])
            if display != 0:
                return

        #we want to show this one, return the dictionary
        return fileInfo

    def showHooksMessage(self):
        KNotification.event("Information", i18n("Software upgrade notifications are available"), KIcon("help-hint").pixmap(QSize(22,22)))

    def hooksActivated(self, activationReason):
        """show the dialogue with messages when user clicks on upgrade hooks systray icon"""
        if activationReason == QSystemTrayIcon.Trigger:
            self.hooksTray.hide()
            dialogue = QDialog()
            if os.path.exists("hooks-dialogue.ui"):
                APPDIR = QDir.currentPath()
            else:
                file = KStandardDirs.locate("appdata", "hooks-dialogue.ui")
                APPDIR = file.left(file.lastIndexOf("/"))
            uic.loadUi(APPDIR + "/hooks-dialogue.ui", dialogue)
            dialogue.image.setPixmap(KIcon("help-hint").pixmap(QSize(48,48)))
            nextButton = dialogue.buttonBox.addButton(i18n("Next"), QDialogButtonBox.AcceptRole)
            nextButton.setIcon(KIcon("go-next"))
            runButton = dialogue.buttonBox.addButton(i18n("Run this action now"), QDialogButtonBox.ActionRole)
            runButton.setIcon(KIcon("system-run"))
            self.connect(runButton, SIGNAL("clicked()"), self.runHookCommand)
            dialogue.buttonBox.button(QDialogButtonBox.Close).setIcon(KIcon("dialog-close"))
            i = 0
            for file in self.fileInfos:
                i = i + 1
                if len(self.fileInfos) == i:
                    nextButton.hide()
                self.terminal = False
                if self.fileInfos[file].has_key("Command"):
                    runButton.show()
                    self.command = self.fileInfos[file]["Command"]
                    if self.fileInfos[file].has_key("Terminal") and self.fileInfos[file]["Terminal"] == "True":
                        self.terminal = True
                else:
                    runButton.hide()
                language = KGlobal.locale().language()
                if self.fileInfos[file].has_key("Name"):
                    if self.fileInfos[file].has_key("Name-" + language):
                        text = self.fileInfos[file]["Name" + language]
                    else:
                        text = self.fileInfos[file]["Name"]
                    dialogue.vbox_message.setText(i18n("<b>%1</b>", text))
                else:
                    dialogue.vbox_message.setText("<b>Update Information</b>")
                if self.fileInfos[file].has_key("Description-" + language):
                    text = self.fileInfos[file]["Description" + language]
                else:
                    text = self.fileInfos[file]["Description"]
                text = text.replace("\n", "")
                if self.fileInfos[file].has_key("GettextDomain"):
                    KGlobal.locale().insertCatalog(self.fileInfos[file]["GettextDomain"])
                    text = i18n(text)
                dialogue.textview_hook.setText(text)
                result = dialogue.exec_()
                #save that it has been seen
                config = KGlobal.config()
                configGroup = config.group("updateNotifications")
                configGroup.writeEntry(file, QVariant(True))
                config.sync()
                if result == QDialog.Rejected:
                    break

    def runHookCommand(self):
        if self.terminal:
            # if command is quoted, invokeTermianl will refuse to interpret it properly
            if self.command.startswith("\"") and self.command.endswith("\""):
                self.command = self.command[1:][:-1]
            KToolInvocation.invokeTerminal(self.command)
        else:
            process = KProcess()
            process.setShellCommand(self.command)
            process.startDetached()

    # Restricted codec install

    def showCodecTray(self, application, package):
        """ when the dbus interface is called, prompt user to install """
        if not self.packages.has_key(package):
            print "update-notifier-kde, request to install unknown restricted package: " + package
            return
        config = KGlobal.config()
        configGroup = config.group("updateNotifications")
        dontShow = configGroup.readEntry("hideCodecsNotifier", QVariant(False))
        if not QFile.exists("/var/lib/dpkg/info/" + package + ".list") and not dontShow.toBool():
            self.application = application
            self.packageCategory = self.packages[package][1]
            if self.packageCategory == MULTIMEDIA_CATEGORY:
                toolTip = i18nc("%1 is application name", "%1 recommends installing packages for extra multimedia functionality.", application)
            elif self.packageCategory == SEMANTIC_DESKTOP_CATEGORY:
                toolTip = i18nc("%1 is application name", "%1 recommends installing packages for extra semantic desktop functionality.", application)
            self.codecsTray.setToolTip(toolTip)
            self.codecsTray.show()
            KNotification.event("Restricted", toolTip, KIcon("download").pixmap(QSize(22,22)))

    def hideCodecs(self):
        """called by right click menu"""
        config = KGlobal.config()
        configGroup = config.group("updateNotifications")
        configGroup.writeEntry("hideCodecsNotifier", QVariant(True))
        config.sync()
        self.codecsTray.hide()

    def codecsActivated(self, activationReason):
        """ user wants to install, show dialogue and install """
        if activationReason != QSystemTrayIcon.Trigger:
            return
        dialogue = KDialog()
        dialogue.setButtons(KDialog.ButtonCode(KDialog.Ok | KDialog.Cancel))
        dialogue.setButtonText(KDialog.Ok, i18n("Install Selected"))
        widget = QWidget(dialogue)
        layout = QVBoxLayout(widget)
        widget.setLayout(layout)
        label = QLabel(widget)
        if self.packageCategory == MULTIMEDIA_CATEGORY:
            label.setText(i18n("For extra multimedia functionality\nselect restricted packages to be installed. "))
        elif self.packageCategory == SEMANTIC_DESKTOP_CATEGORY:
            label.setText(i18n("For extra semantic desktop functionality\nselect restricted packages to be installed. "))
        layout.addWidget(label)
        listWidget = QListWidget(widget)
        layout.addWidget(listWidget)
        dialogue.setMainWidget(widget)
        
        for package in self.packages.keys():
            if not QFile.exists("/var/lib/dpkg/info/" + package + ".list") and self.packages[package][1] == self.packageCategory:
                item = QListWidgetItem(self.packages[package][0])
                item.setCheckState(Qt.Checked)
                item.setToolTip(package)
                listWidget.addItem(item)
        
        if listWidget.count() > 0:
            result = dialogue.exec_()

            if result == QDialog.Accepted:
                toInstall = ""
                for index in range(listWidget.count()):
                    item = listWidget.item(index)
                    if item.checkState() == Qt.Checked:
                        toInstall = toInstall + unicode(item.toolTip()) + " "
                        
                #KProcess.startDetached("/usr/lib/kde4/libexec/kdesu", ["install-package --install " + toInstall])
                self.process = KProcess()
                self.process.setProgram("/usr/lib/kde4/libexec/kdesu", ["install-package --install " + toInstall])
                self.connect(self.process, SIGNAL("finished(int, QProcess::ExitStatus)"), self.processFinished)
                self.process.start()

    def processFinished(self, exitCode, exitStatus):
	if not exitCode:
        	self.codecsTray.hide()
	        KNotification.event("Restricted", i18n("You will need to restart %1 to use the new functionality", self.application), KIcon("system-software-update").pixmap(QSize(22,22)))

class DBusInterface(dbus.service.Object):
    def __init__(self, ui):
        self.ui = ui
        dbus.mainloop.qt.DBusQtMainLoop(set_as_default=True)
        try:
            bus = dbus.SessionBus()
        except:
            print >> sys.stderr, "%s: failed to connect to system D-Bus" % PROGRAM_NAME
            sys.exit (1)

        bus_name = dbus.service.BusName("org.kubuntu.restrictedInstall", bus=bus)
        dbus.service.Object.__init__(self, bus_name, "/org/kubuntu/restrictedInstall")
        
    @dbus.service.method("org.kubuntu.restrictedInstall", in_signature='ss', out_signature='')
    def installRestricted(self, application, package):
        self.ui.showCodecTray(application, package)

if __name__ == "__main__":
    """start the application.  TODO, clever things here to not start the GUI until it has to"""
    appName     = "update-notifier-kde"
    catalogue   = "update-notifier-kde"
    programName = ki18n("Update Notifier")
    version     = "1.1"
    description = ki18n("An applet to show a systray icon when apt has software updates to be installed, when Apport has crash reports, for reboot notification and for upgrade hook messages.")
    license     = KAboutData.License_GPL
    copyright   = ki18n("2008 Canonical Ltd")
    text        = KLocalizedString()
    homePage    = "http://launchpad.net/adept"
    bugEmail    = ""

    aboutData   = KAboutData(appName, catalogue, programName, version, description,
                                license, copyright, text, homePage, bugEmail)

    aboutData.addAuthor(ki18n("Jonathan Riddell"), ki18n("Author"))

    options = KCmdLineOptions()
    options.add("d").add("devel-release", ki18n("Offer to upgrade to the latest devel release, if possible"))
    options.add("p").add("proposed", ki18n("Offer to upgrade using latest proposed version of the release upgrader, if possible"))
    options.add("u").add("dist-upgrade", ki18n("Offer to upgrade to the latest release, if possible"))
    options.add("c").add("check-new-packages", ki18n("Check for updated packages"))

    KCmdLineArgs.init(sys.argv, aboutData)
    KCmdLineArgs.addCmdLineOptions(options)

    app = KApplication()
    app.setQuitOnLastWindowClosed(False)
    app.setWindowIcon(KIcon("system-software-update"))
    if app.isSessionRestored():
        sys.exit(1)
    args = KCmdLineArgs.parsedArgs()
    distUpgrade = False
    useDevelopmentRelease = False
    useProposed = False
    checkNewPackages = False
    if args.isSet("dist-upgrade"):
        distUpgrade = True
    if args.isSet("devel-release"):
        useDevelopmentRelease = True
    if args.isSet("proposed"):
        useProposed = True
    if args.isSet("check-new-packages"):
        checkNewPackages = True

    applet = App(distUpgrade, useDevelopmentRelease, useProposed, checkNewPackages)
    sys.exit(app.exec_())
