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

'''GUI (PyQt4) Installer for Scratchbox1 based Maemo SDK'''

# This file is part of Maemo SDK
#
# Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
#
# Contact: juha-pekka.jokela@tieto.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 os
import re
import sys
import pwd
import grp
import time
import socket
import urllib
import tempfile
import random
import signal
import traceback
import subprocess
from optparse import OptionParser


def Which(program):
    '''Returns path to given binary, if it can be found.'''
    def is_exe(fpath):
        return os.path.exists(fpath) and os.access(fpath, os.X_OK)

    fpath, fname = os.path.split(program)
    if fpath:
        if is_exe(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file

    return None


def install_package(package, askuser = True):
    '''Tries to install specified debian package (or defined rpm equivalent), returns True on success'''
    # Will ask user for confirmation, depending on "askuser" value

    # Mappings for matching fedora / suse packages from debian package name
    rpm_pkg_name = {'wget': 'wget', 'python-qt4': 'PyQt4', 'xserver-xephyr': 'xorg-x11-server-Xephyr'}
    suse_pkg_name = {'wget': 'wget', 'python-qt4': 'python-qt4', 'xserver-xephyr': 'xorg-x11-server-extra'}
    # Ask user, if requested
    if askuser:
        reply = raw_input('Do you want to install missing package "%s"? (Type "y" to install) ' % package)
    else:
        reply = 'y'

    if reply.lower() in ['y', 'yes']:
        if os.path.isfile('/etc/debian_version') and Which('apt-get'):
            # Checking for /etc/debian_version will make installation to be tried on yum first, if
            # user has f.ex. installed apt on fedora.
            if askuser:
                command = ['apt-get', 'install', package]
            else:
                command = ['apt-get', '-y', 'install', package]

        elif Which('yum'):
            if package in rpm_pkg_name:
                if askuser:
                    command = ['yum', '-y', 'install', rpm_pkg_name[package]]
                else:
                    command = ['yum', 'install', rpm_pkg_name[package]]
            else:
                print 'No fedora equivalent defined for debian package "%s", not installing' % package
                return False

        elif Which('zypper'):
            if package in suse_pkg_name:
                if askuser:
                    command = ['zypper', 'in', suse_pkg_name[package]]
                else:
                    command = ['zypper', '-y', 'in', suse_pkg_name[package]]
            else:
                print 'No suse equivalent defined for debian package "%s", not installing' % package
                return False
        elif Which('apt-get'):
            # apt-get found, but /etc/debian_version missing. Ask user what to do.
            if askuser:
                reply = raw_input('apt-get found, but /etc/debian_version is missing. Do you want to use'
                                  'apt-get to install missing package %s?', package)
            else:
                reply = 'y'
            if reply.lower() in ['y', 'yes']:
                command = ['apt-get', 'install', package]
            else:
                return False
        else:
            print 'No compatible package manager found (only apt-get, yum and zypper are supported)'
            return False
    else:
        return False

    # Don't print to console if not in interactive mode
    if askuser:
        print 'Executing command:', command

    p = subprocess.Popen(command)
    p.wait()
    if p.returncode:
        print 'Automatic installation of package "%s" failed.' % command[2]
        return False

    return True


askedinstall = False

while (True):
    try:
        from PyQt4 import QtGui
        from PyQt4 import QtCore
        from PyQt4 import QtNetwork

    except ImportError, e:
        # Can raise AttributeError if the version of PyQt4 is prior to Qt 4.3,
        # since according to
        # http://doc.trolltech.com/4.4/porting4-overview.html#wizard-dialogs-qwizard
        # QWizard and siblings were added to that version of Qt4. Systems that have
        # too old Qt and bindings by default: Debian Etch.

        print "Python Qt4 bindings are not found!"
        if not askedinstall:
            askedinstall = True
            if install_package('python-qt4'):
                print "Successfully installed package python-qt4"
            else:
                print "Failed to install Python Qt4 bindings"
                sys.exit(1)
    else:
        break


try:
    from PyQt4 import QtWebKit

except ImportError, e:
    # Only WebKit is missing, if it was available, it would have been installed
    # with Qt Python bindings package. Run installation without WebKit functionality.
    HAVE_WEBKIT = False

else:
    HAVE_WEBKIT = True

# wget needed by command line installers, make sure it exists. If not, ask if user
# wants to install it automatically.
if not Which('wget'):
    print "wget not found!"
    if not install_package('wget'):
        sys.exit(1)
    # Check once again to make sure it really does exist now
    if not Which('wget'):
        print "wget still missing, aborting installation."
        sys.exit(1)



# command line options and arguments
OPTIONS = None
ARGS = None
OPT_PARSER = None

PROXY = dict()

# logger, initialized in main
LOG = None

XEPHYR_SHORTCUT_FILENAME = "start_xephyr.sh"

# names shown to the user (what is installed/removed and installer's name)
PRODUCT_NAME = 'Maemo %s SDK' % ("5.0")
PRODUCT_NAME_SHORT = 'SDK'
SB_NAME = 'Scratchbox'

MY_NAME = '%s Installer' % PRODUCT_NAME
MY_VERSION = "0.1.1 ($Revision: 2043 $)"

# SDK time & space consumption: these are very rough
INST_CONS_TIME = '20 minutes'
INST_CONS_SPACE = "3GB"

# where scratchbox is found
SB_PATH = "/scratchbox"

# scratchbox group
SB_GROUP = "sbox"

# window size
WINDOW_WIDTH = 720
WINDOW_HEIGHT = 540

USE_IMAGES = True

MAEMO_LINKS_FILENAME = "Maemo 5 links.html"
XEPHYR_DESKTOP_ENTRY_FILENAME = "xephyr.desktop"

# Base URL's
INSTALLER_BASE_URL = "http://repository.maemo.org/stable/5.0"
EULA_BASE_URL = "http://tablets-dev.nokia.com/eula"

# URL's of installers (can be set to local file)
SB_INSTALLER_URL = "%s/maemo-scratchbox-install_5.0.sh" % INSTALLER_BASE_URL
SDK_INSTALLER_URL = "%s/maemo-sdk-install_5.0.sh" % INSTALLER_BASE_URL

EULA_URL = "%s/index.php" % EULA_BASE_URL
TOKEN_URL = "%s/token.php" % EULA_BASE_URL

ALLOW_UPGRADE = True
# Install following packages (space separated) before apt-get dist-upgrade
UPDATE_INSTALL_PACKAGES = 'maemo-sdk-opt'
UPDATE_SCRIPT_FN = '/tmp/maemo-sdk-install-wizard_upgrade.sh'

IMAGES = ['http://tablets-dev.nokia.com/sdk_installer/applications.png',
          'http://tablets-dev.nokia.com/sdk_installer/desktop.png',
          'http://tablets-dev.nokia.com/sdk_installer/TaskSwitcher.png']

INSTALL_OPTIONS = [
            ["&Minimal rootstraps only", "none"],
            ["&Runtime environment", "maemo-sdk-runtime"],
            ["&Development environment", "maemo-sdk-dev"],
            ["Debu&g environment", "maemo-sdk-debug"]]

DEFAULT_INSTALL_OPTION = 2 # checked by default

BINPATHS = ["/usr/local/bin", "usr/bin"]

# return codes
RC_NO_ERROR = 0
RC_OUTOFDISKSPACE = 2

# default target names for this release of SDK, user can choose different
# prefix if default targets already exist in scratchbox
TARGET_PREFIX = "FREMANTLE"
TARGET_POSTFIX_X86 = "_X86"
TARGET_POSTFIX_ARMEL = "_ARMEL"
TARGET_X86 = TARGET_PREFIX + TARGET_POSTFIX_X86
TARGET_ARMEL = TARGET_PREFIX + TARGET_POSTFIX_ARMEL

# license text for the SDK (non-ASCII quotes replaced with ASCII)
SDK_LICENSE = \
'''1) IMPORTANT: READ CAREFULLY BEFORE INSTALLING, DOWNLOADING, OR USING THE
SOFTWARE DEVELOPMENT KIT ("SDK" AS DEFINED BELOW) AND/OR SOFTWARE INCLUDED INTO
THE SDK

2) The SDK comprises of a) some software copyrighted by Nokia Corporation or
third parties in binary form (collectively "Licensed Software") and/or b) Open
Source Software in binary and source code form.

3) Licensed Software (including, without limitation, the downloading,
installation and/or the use thereof) is subject to, and licensed to you under,
the Nokia Software Development Kit Agreement, which you have to accept if you
choose to download the Licensed Software. Licensed Software is distributed to
you only in binary form.

4) The SDK is provided to you "AS IS" and Nokia, its affiliates and/or its
licensors do not make any representations or warranties, express or implied,
including, without any limitation, the warranties of merchantability or
fittness for a particular purpose, or that the SDK will not infringe any any
third party patents, copyrights, trademarks or other rights, or that the SDK
will meet your requirements or that the operation of the SDK will be
uninterrupted and/or error-free. By downloading and/or using the SDK you accept
that installation, any use and the results of the installation and/or happens
and is solely at your own risk and that Nokia assumes no liability whatsoever
for any damages that you may incur or suffer in connection with the SDK and/or
the installation or use thereof.

5) The Open Source Software is licensed and distributed under the GNU General
Public License (GPL), the GNU lesser General Public License (LGPL, aka. The GNU
Library General Public License) and/or other copyright licenses, permissions,
notices or disclaimers containing obligation or permission to provide the
source code of such software with the binary / executable form delivery of the
said software. Any source code of such software that is not part of this
delivery is made available to you in accordance with the referred license terms
and conditions on http://www.maemo.org. Alternatively, Nokia offers to provide
any such source code to you on CD-ROM for a charge covering the cost of
performing such distribution, such as the cost of media, shipping and handling,
upon written request to Nokia at:

Source Code Requests
Nokia Corporation
P.O.Box 407
FIN00045 Nokia Group
Finland.

This offer is valid for a period of three (3) years.

The exact license terms of GPL, LGPL and said certain other licenses, as well
as the required copyright and other notices, permissions and acknowledgements
are reproduced in and delivered to you as part of the referred source code.
'''


class ImageHandler(object):
    '''Class for handling images.'''

    def __init__(self, imagelist):
        '''Constructor'''
        self.imagedata = None
        self.imagelist = imagelist

    def __del__(self):
        '''Destructor'''

    def downloadImage(self, i):
        '''Download specified image, currently blocks untill finished'''
        proxy = get_proxy('http')
        try:
            handle = urllib.urlopen(self.imagelist[i], proxies = proxy)
            self.imagedata = handle.read()

        except Exception, e:
            print "Failed to load image:", e
            self.imagedata = None
            return False
        else:
            return True


    def getImage(self):
        '''Return image pixmap, if already loaded'''
        if self.imagedata:
            pixmap = QtGui.QPixmap()
            pixmap.loadFromData(self.imagedata)
            return pixmap
        else:
            return None


def get_all_usernames():
    '''Returns non-system usernames if can get the system limits for normal
    user's UID. If not then returns all of the usernames in the system. The
    usernames are returned in a list.'''
    user_conf_file = "/etc/adduser.conf"

    # inclusive range of UIDs for normal (non-system) users
    # by default this is the maximum possible range, that includes all users
    first_uid = 0
    last_uid = sys.maxint

    found_first_uid = False
    found_last_uid = False 

    # try to get the UID range
    if os.path.isfile(user_conf_file):
        for l in open(user_conf_file):

            l = l.strip()

            if l.startswith("FIRST_UID"):
                first_uid = int(l.split("=")[1])
                found_first_uid = True

            if l.startswith("LAST_UID"):
                last_uid = int(l.split("=")[1])
                found_last_uid = True

            # both found
            if found_first_uid and found_last_uid:
                break

    usernames = []

    for i in pwd.getpwall():
        # if UID not within the range, then skip this user
        if not first_uid <= i.pw_uid <= last_uid:
            continue
        usernames.append(i.pw_name)

    usernames.sort()

    return usernames


def get_default_username():
    '''Returns username of the user that invoked sudo or su to run this
    script. Returns None if couldn't get such entry.'''

    # try to get the user that ran sudo
    username = os.getenv('SUDO_USER')

    # or user that ran su
    if not username:
        username = os.getenv('USERNAME')

    if username:

        # installing SDK as root is not allowed, also the list of users does
        # not show system users
        if username == 'root':
            LOG("Omitting default user 'root'")
            return None

        # verify that this user actually exists
        try:
            pwd.getpwnam(username)
        except KeyError:
            LOG("User %s not in password file!" % username)
            return None

    return username


def xephyr_has_kb():
    '''Tries to figure out whether we should use -kb with Xephyr'''
    if Which('Xephyr'):
        command = "Xephyr -help 2>&1 | grep '+kb'"
        p = subprocess.Popen(command, shell = True, stdout=subprocess.PIPE)
        stdout, stderr = p.communicate()
        if stdout:
            return True

    return False


def get_scratchbox_users():
    #Returns list of scratchbox users
    userlist = []
    userdir = '%s/users' % SB_PATH
    #If user directory doesn't exist, there can be no users.
    if os.path.isdir(userdir):
        p = subprocess.Popen('ls %s' % userdir,
                             stdout = subprocess.PIPE,
                             shell = True)
        userlist = p.communicate()[0].split("\n")
        #Remove invalid entries, if any
        for user in userlist:
            if not os.path.isdir("%s/%s" % (userdir, user)):
                userlist.remove(user)

        #Remove last empty line
        userlist.pop()

    return userlist


def uninstall_scratchbox(has_64bit):
    '''Tries to uninstall scratchbox from the system.'''

    #Uninstallation needs to be performed differently on 64bit
    if has_64bit:
        packages_64bit = ['scratchbox-devkit-doctools', 
                          'scratchbox-devkit-perl',
                          'scratchbox-devkit-debian',
                          'scratchbox-devkit-svn',
                          'scratchbox-devkit-git',
                          'scratchbox-devkit-apt-https',
                          'scratchbox-devkit-qemu',
                          'scratchbox-core']
        for package in packages_64bit:
            exec_cmd('dpkg --purge %s' % package)
    else:
        #Easier on 32bit
        exec_cmd('apt-get remove -y --purge scratchbox-libs')


def get_user_targets(user):
    '''Return list of targets for specified user'''
    targets = []
    preexec = lambda: set_guid(pwd.getpwnam(user),
                               True)
    p = subprocess.Popen('%s/tools/bin/sb-conf list --targets' % SB_PATH,
                         stdout = subprocess.PIPE,
                         preexec_fn = preexec,
                         shell = True)
    targets = p.communicate()[0].split("\n")
    #Remove lines which do not point to valid directory (including errors / warnings)
    for target in targets:
        if not os.path.isdir("%s/users/%s/targets/%s" % (SB_PATH, user, target)):
            targets.remove(target)
    #Remove last empty line, as it's not removed by previous test
    targets.pop()
    return targets


def has_user_active_sessions(user):
    '''Returns True if user has active scratchbox sessions'''
    lines = 0
    if os.path.isfile("%s/tools/bin/sb-conf" % SB_PATH):
        p = subprocess.Popen('su %s -c "%s/tools/bin/sb-conf list --sessions"' % (user, SB_PATH),
                             stdout = subprocess.PIPE,
                             shell = True)
        sessions = p.communicate()[0].split("\n")
        for session in sessions:
            if re.match("^/dev/pts/[0-9]+:\s+[0-9]+", session):
                lines += 1

    return lines > 1


def has_user_group(user, group):
    '''Returns True if user already is part of specified group'''
    '''group is checked from /etc/group instead of "groups" command'''
    p = subprocess.Popen('grep %s /etc/group' % group,
                         stdout = subprocess.PIPE,
                         shell = True)
    output = p.communicate()[0]
    groupusers = output[output.rfind(':') + 1:len(output) - 1].split(',')
    for groupuser in groupusers:
        if groupuser == user:
            return True

    return False


def add_user_to_group(user, group):
    '''Adds specified user to specified group'''
    p = subprocess.Popen('usermod -a -G %s %s' % (group, user),
                         shell = True)
    p.wait()


def remove_scratchbox_target(user, target):
    '''Removes specified target from specified user, if it exists'''
    #Doesn't try to remove targets, for which directory is not found, so 
    #error messages, and empty target strings will be ignored.
    if target and os.path.isdir("%s/users/%s/targets/%s" % (SB_PATH, user, target)):
        LOG("Removing target %s" % (target))
        exec_cmd('%s/tools/bin/sb-conf remove -f %s' % (SB_PATH, target),
                 username = user,
                 set_sbox_gid = True)


def get_user_home_dir(user):
    '''returns home directory for the specified user, or None'''
    homedir = os.path.expanduser('~%s' % (user))
    if os.path.isdir(homedir):
        return homedir
    else:
        return None


def get_user_desktop_dir(user):
    '''returns desktop directory for the specified user, or None'''
    desktopdir = None
    homedir = get_user_home_dir(user)
    if homedir:
        xdg_fn = "%s/.config/user-dirs.dirs" % (homedir)
        if os.path.isfile(xdg_fn):
            # See if we have XDB_DESKTOP_DIR entry in xdg's config file
            xdg_file = open(xdg_fn, "r")
            xdg_text = xdg_file.read().splitlines()
            xdg_file.close()
            for line in xdg_text:
                line = line.strip()
                if not line.startswith("#"):
                    index = line.find("XDG_DESKTOP_DIR")
                    if index is not -1:
                        splitline = line[index:].split("=")
                        desktopdir = splitline[1].strip("\" ")
                        desktopdir = desktopdir.replace("$HOME", homedir)

        else:
            # Not found, see if $HOME/Desktop exists, and use that instead.
            if os.path.isdir("%s/Desktop" % (homedir)):
                desktopdir = "%s/Desktop" % (homedir)
        if desktopdir:
            if os.path.isdir(desktopdir):
                return desktopdir
        else:
            # Desktop directory not found, use home directory instead
            if homedir:
                return homedir
    return None


def parse_options():
    '''Parses the command line options'''
    global OPTIONS
    global ARGS
    global OPT_PARSER

    usage = 'Usage: %prog [options]' + \
'''

  %s.

  Installs %s.''' % (MY_NAME, PRODUCT_NAME)

    OPT_PARSER = OptionParser(usage = usage, version = "%prog " + MY_VERSION)
    OPT_PARSER.add_option("-a", dest="sourceslistfile",
                          help="Specify alternative sources.list file for both targets.", metavar="FILE")
    (OPTIONS, ARGS) = OPT_PARSER.parse_args()

def set_proxy(proxy):
    '''Sets http_proxy to specified'''
    global PROXY
    PROXY = dict([('http', proxy)])

def get_proxy(protocol):
    '''returns proxy for specified protocol. On some systems PROXY['http'] can be None, and it can cause problems'''
    global PROXY
    proxy = None
    if PROXY:
        if protocol in PROXY:
            if PROXY[protocol]:
                proxy = PROXY
    return proxy


def download_file(url, chown_username = None, set_exec_bit = True):
    '''Downloads a file into temporary location. Returns that location.
    url = URL to download from
    chown_username = if set, the ownership of the downloaded file will be
                     given to this user
    set_exec_bit = if True execute mode bit of the downloaded file will be
                   set'''
    LOG("Downloading %s" % url)
    filename = None
    try:
        split = os.path.splitext(url)
        suf = split[len(split)-1]
        proxy = get_proxy('http')
        fd = tempfile.mkstemp(suffix = suf)
        filehandle = os.fdopen(fd[0], "w")
        filename = fd[1]
        LOG("Filename:%s" % filename)
        urlhandle = urllib.urlopen(url, proxies = proxy)
        filehandle.write(urlhandle.read())
        urlhandle.close()
        filehandle.close()

    except Exception, e:
        LOG("Error downloading file %s: %s" % (url, e))
        raise e

    else:
        LOG("Saved download into %s" % filename)

        if set_exec_bit:
            os.chmod(filename, 0700)

        if chown_username:
            pwd_ent = pwd.getpwnam(chown_username)
            os.chown(filename, pwd_ent.pw_uid, pwd_ent.pw_gid)

    return filename


def bool_to_yesno(bool_value):
    '''Return yes if Boolean value is true, no if it is false'''
    if bool_value:
        return 'Yes'
    else:
        return 'No'


def file_append_lines(filename, lines):
    '''Appends list of lines into a file. Newlines are added to each appended
    line.
    filename = name of the file
    lines = list of lines'''

    if not os.path.isfile(filename):
        LOG("WARNING! Appending to non-existing file (%s), will be created!" %
            (filename))

    LOG("Appending into file %s lines %s" % (filename, lines))

    fo = open(filename, "a")

    try:
        for line in lines:
            fo.write(line + '\n')

    finally:
        fo.close()


def set_guid(pwd_ent, set_sbox_gid = False):
    '''Changes the effective, real and saved set-user-ID user and group IDs,
    should be used to permanently drop root privileges.  Also sets HOME
    environment variable since maemo-sdk command extensively uses that
    variable.

    pwd_ent = Password database entry of the user whose credentials will be
    used
    set_sbox_gid = The GID will be set to that of the sbox. This is required
                   to run the SDK installer and any other scratchbox related
                   command. sg cannot be used since it does not return the
                   exit status of the executed process. Another option is
                   newgrp command.'''

    if set_sbox_gid:
        gid = grp.getgrnam(SB_GROUP).gr_gid
    else:
        gid = pwd_ent.pw_gid

    os.setgid(gid)
    os.setuid(pwd_ent.pw_uid)
    os.environ['HOME'] = pwd_ent.pw_dir
    os.environ['USER'] = pwd_ent.pw_name # SDK installer needs this


class CmdExecError(Exception):
    '''Exception raised when execution of a command fails.'''
    pass


def exec_cmd(command, username = None, set_sbox_gid = False, send_text = None, donotwait = False, return_errorcode = False):
    '''Executes a command and raises exception if command fails.
    username = if set, will run command with the credentials (UID & GID) of
               that user.
    set_sbox_gid = sets the GID of executed command to sbox, so that scratchbox
                   commands can be executed. This is naturally only used when
                   the username is specified, since root is not allowed to run
                   scratchbox commands.
    send_text = Text to send to stdin of the process before waiting for it
    return_errorcode = return errorcode after processing'''

    if username:
        LOG("Executing as user %s: %s" % (username, command))
        if set_sbox_gid:
            #Check if user is already part of sbox group (in which case we don't have to use the newgrp hack)
            p = subprocess.Popen('groups %s|grep %s' % (username, SB_GROUP),
                                 stdout = subprocess.PIPE,
                                 shell = True)
            output = p.communicate()[0]
            if not output:
                #Use newgrp as user isn't part of sbox group yet
                #First newgrp sets sbox as users default group, second one restores the original one.
                LOG("User not part of %s group, using newgrp" % SB_GROUP)
                command = "newgrp %s << 'END1'\nnewgrp << 'END2'\n%s\nEND2\nEND1\n" % (SB_GROUP, command)
        #Execute as user with su
        command = "su %s -c \"%s\"" % (username, command)

    LOG("Final command:%s" % command)
    if send_text:
        p_stdin = subprocess.PIPE
    else:
        p_stdin = None # as default in Popen

    p = subprocess.Popen(command,
                         stdin = p_stdin,
                         stdout = LOG.fo_log,
                         stderr = subprocess.STDOUT,
                         shell = True)
    if send_text:
        p.communicate(send_text)

    if donotwait:
        return
    else:
        p.wait()
    if return_errorcode:
        return p.returncode

    if p.returncode:
        raise CmdExecError("Giving up, because failed to: %s" % command)


def scratchbox_target_exists(username, target):
    '''Returns True if specified target for specified username exists in
    scratchbox.'''
    return os.path.isdir("%s/users/%s/targets/%s" %
                         (SB_PATH, username, target))


def scratchbox_prefix_exist(username, prefix):
    '''Returns True if either armel or x86 target with specified prefix exists
    in scratchbox. The SDK installer creates targets by taking prefix and
    appending architecture.'''

    return scratchbox_target_exists(username, prefix + TARGET_POSTFIX_X86) or \
        scratchbox_target_exists(username, prefix + TARGET_POSTFIX_ARMEL)


class Logger(object):
    '''Class for logging.'''

    def __init__(self):
        '''Constructor'''

        script_name = os.path.basename(sys.argv[0])

        # file name of the log file: script name, with extenstion replaced
        self.fn_log = "/tmp/%s.log" % script_name[:script_name.rfind('.')]

        self.fo_log = None

        try:
            self.fo_log = open(self.fn_log, 'w')
        except:
            print ("Could not open log file %s!" % (self.fn_log,))
            raise

        self.log("Python version: %s" % repr(sys.version))
        self.log("Installer version: %s" % MY_VERSION)

    def __del__(self):
        '''Destructor'''
        if self.fo_log:
            self.fo_log.close()

    def log(self, msg):
        """Writes a log message."""
        self.fo_log.write("V [%s]: %s\n" % 
                          (time.strftime("%H:%M:%S %d.%m.%Y"), msg))
        self.fo_log.flush()

    def __call__(self, *args, **kwds):
        '''Shortcut for log'''
        self.log(*args, **kwds)

    def log_exc(self):
        '''Writes current exception information into the log file'''
        fmt = '-' * 5 + " %s " + '-' * 5

        self.log(fmt % "Begin logging exception")
        traceback.print_exc(file = self.fo_log)
        self.log(fmt % "End logging exception")


class HostInfo(object):
    '''Information about host'''

    def __init__(self):
        '''Constructor'''

        # whether host already has scratchbox installed
        self.__has_scratchbox = os.path.isfile("%s/etc/scratchbox-version" %(SB_PATH))
        self.__scratchbox_op_name = self.__get_scratchbox_op_name()
        machines_64bit = ["x86_64"]
        machine = os.uname()[4]

        if machine in machines_64bit:
            self.__has_64bit = True
        else:
            self.__has_64bit = False

        if Which('Xephyr'):
            self.__has_xephyr = True
        else:
            self.__has_xephyr = False

        # Should we consider apt as our primary way of installing packages
        if os.path.isfile('/etc/debian_version') and Which('apt-get'):
            self.__has_apt_get = True
        else:
            self.__has_apt_get = False

        if Which('yum'):
            self.__has_yum = True
        else:
            self.__has_yum = False

        if Which('zypper'):
            self.__has_zypper = True
        else:
            self.__has_zypper = False

        LOG("Got host info: %s" % self)

    @property
    def has_scratchbox(self):
        return self.__has_scratchbox

    @property
    def has_xephyr(self):
        return self.__has_xephyr

    @property
    def scratchbox_op_name(self):
        return self.__scratchbox_op_name

    @property
    def has_64bit(self):
        return self.__has_64bit

    @property
    def has_apt_get(self):
        return self.__has_apt_get

    @property
    def has_yum(self):
        return self.__has_yum

    @property
    def has_zypper(self):
        return self.__has_zypper

    def __get_scratchbox_op_name(self):
        '''Returns string of what operation is needed to get scratchbox to this
        host: install or upgrade'''

        if self.has_scratchbox:
            return "Upgrade"

        else:
            return "Install"

    def __str__(self):
        '''String representation method.'''
        return str(self.__dict__)


# field names for wizard and its siblings
FNLicenseAccept         = "license_accept"          # bool
FNInstallSDK            = "install_sdk"             # bool
FNUpgradeSDK            = "upgrade_sdk"             # bool
FNInstallSB             = "install_sb"              # bool
FNUninstall             = "uninstall"               # bool
FNInstallNokiaApps      = "install_nokia_apps"      # bool
FNSelectedUsername      = "selected_username"       # string
FNTargetX86Exist        = "target_x86_exist"        # bool
FNTargetArmelExist      = "target_armel_exist"      # bool
FNRemoveTargets         = "targets_remove"          # bool
FNTargetPrefix          = "targets_prefix"          # string
FNSDKInstMOptArg        = "sdk_m_opt_arg"           # string
FNSDKInstMOptArgText    = "sdk_m_opt_arg_txt"       # string
FNInstallNokiaBins      = "nokiabins_install"       # bool
FNNokiaBinsRepo         = "nokiabins_repo"          # string
FNInstallXephyr         = "install_xephyr"          # bool
FNInstallXephyrShortcut = "install_xephyr_shortcut" # bool
FNInstallDesktopLinks   = "install_desktop_links"   # bool
FNEasyInstall           = "easy_install"            # bool
FNCreateSbHomeShortcut  = "create_sb_home_shortcut" # bool
FNForceInstallLowSpace  = "force_install_low_space" # bool

class ProxyPage(QtGui.QWizardPage):
    '''Proxy settings page'''
    def __init__(self):
        '''Constructor'''
        QtGui.QWizardPage.__init__(self)
        self.layout = QtGui.QVBoxLayout()
        self.setLayout(self.layout)

    def initializePage(self):
        self.setCommitPage(True)
        self.setTitle('Proxy Settings')
        self.setSubTitle(" ")

        proxy_host_port = QtGui.QWidget()
        self.proxy_host = QtGui.QLineEdit()
        txt = "Enter hostname for your proxy"
        self.proxy_host.setToolTip(txt)
        self.proxy_host.setWhatsThis(txt)
        self.proxy_host_label = QtGui.QLabel("Proxy host:")

        self.proxy_port = QtGui.QLineEdit()
        txt = "Enter port number for your proxy"
        self.proxy_port.setToolTip(txt)
        self.proxy_port.setWhatsThis(txt)
        intvalidator = QtGui.QIntValidator(1, 65535, self)
        self.proxy_port.setValidator(intvalidator)
        self.proxy_port_label = QtGui.QLabel("Proxy port:")

        self.proxy_username = QtGui.QLineEdit()
        txt = "Enter username for your proxy"
        self.proxy_username.setToolTip(txt)
        self.proxy_username.setWhatsThis(txt)
        self.proxy_username_label = QtGui.QLabel("Proxy username:")

        self.proxy_password = QtGui.QLineEdit()
        txt = "Enter password for your proxy"
        self.proxy_password.setToolTip(txt)
        self.proxy_password.setWhatsThis(txt)
        self.proxy_password.setEchoMode(QtGui.QLineEdit.Password)
        self.proxy_password_label = QtGui.QLabel("Proxy password:")

        self.login_enable = QtGui.QCheckBox("Show login")
        self.proxy_button = QtGui.QPushButton("&Confirm")
        self.connect(self.proxy_button,
                     QtCore.SIGNAL("clicked()"), self.confirmClicked)
        self.connect(self.proxy_button,
                     QtCore.SIGNAL("clicked()"), self.confirmClicked)

        self.connect(self.login_enable, QtCore.SIGNAL("toggled(bool)"),
                     self.loginEnableToggled)

        proxy_host_port_layout = QtGui.QGridLayout()
        proxy_host_port_layout.addWidget(self.proxy_host_label, 0, 0)
        proxy_host_port_layout.addWidget(self.proxy_host, 0, 1)
        proxy_host_port_layout.addWidget(self.proxy_port_label, 1, 0)
        proxy_host_port_layout.addWidget(self.proxy_port, 1, 1)
        proxy_host_port_layout.addWidget(self.proxy_username_label, 2, 0)
        proxy_host_port_layout.addWidget(self.proxy_username, 2, 1)
        proxy_host_port_layout.addWidget(self.proxy_password_label, 3, 0)
        proxy_host_port_layout.addWidget(self.proxy_password, 3, 1)
        proxy_host_port.setLayout(proxy_host_port_layout)
        self.loginEnableToggled(False)
        proxy_settings = QtGui.QWidget()
        proxy_settings_layout = QtGui.QVBoxLayout()
        proxy_settings_layout.addWidget(self.login_enable)
        proxy_settings_layout.addWidget(proxy_host_port)
        proxy_settings_layout.addWidget(self.proxy_button)
        proxy_settings.setLayout(proxy_settings_layout)
        self.layout.addWidget(proxy_settings)
        self.notetext = QtGui.QLabel("Failed to connect, please check your proxy settings.")
        self.notetext.setWordWrap(True)
        self.notetext.setVisible(False)
        self.layout.addWidget(self.notetext)

    def loginEnableToggled(self, checked):
        self.proxy_username_label.setVisible(checked)
        self.proxy_username.setVisible(checked)
        self.proxy_password_label.setVisible(checked)
        self.proxy_password.setVisible(checked)

    def confirmClicked(self):
        host_str = str(self.proxy_host.text())
        port_str = str(self.proxy_port.text())
        if self.login_enable.isChecked():
            user_str = str(self.proxy_username.text())
            pswd_str = str(self.proxy_password.text())
            if user_str and pswd_str:
                loginstring = "%s:%s@" %(user_str, pswd_str)
            else:
                loginstring = ""
        else:
            loginstring = ""

        if port_str != '' and host_str != '':
            if host_str.startswith('http://'):
                #Remove http:// from beginning, we will add it with login info
                host_str = host_str[7:]

            proxystring = 'http://%s%s:%s' % (loginstring, host_str, port_str)
            os.environ['http_proxy'] = proxystring
            set_proxy(proxystring)
        else:
            set_proxy(None)
            os.environ['http_proxy'] = ('')

        if self.wizard().imageHandler.downloadImage(random.randint(0, len(IMAGES)-1)):
            self.wizard().next()
            self.emit(QtCore.SIGNAL("completeChanged()"))
        else:
            self.notetext.setVisible(True)

    def isComplete(self):
        '''Installer will automatically move to next page, when connection works.'''
        return False


class IntroPage(QtGui.QWizardPage):
    '''Introduction page'''

    def __init__(self):
        '''Constructor'''

        QtGui.QWizardPage.__init__(self)

        self.setTitle('%s Installation Wizard' % PRODUCT_NAME)
        self.setSubTitle(" ")

        intro_label = QtGui.QLabel("This installer will guide you through the steps "
                                   "needed to install %s on your development "
                                   "machine. The installation will take approximately %s  "
                                   "(depending on download speed) and about %s of disk space on system root. \n\n"

                                   "It will install Scratchbox cross compilation environment togther with Maemo 5 "
                                   "development files on your host system.\n" %
                                   (PRODUCT_NAME, INST_CONS_TIME, INST_CONS_SPACE))

        intro_label.setWordWrap(True)

        self.layout = QtGui.QVBoxLayout()
        self.layout.addWidget(intro_label)


    def initializePage(self):
        if USE_IMAGES:
            pixmap = self.wizard().imageHandler.getImage()
            if pixmap:
                label = QtGui.QLabel()
                label.setPixmap(pixmap)
                label.setAlignment(QtCore.Qt.AlignCenter)
                self.layout.addWidget(label)

        self.setLayout(self.layout)


class LevelPage(QtGui.QWizardPage):
    '''Introduction page - select standard or custom installation'''

    def __init__(self, host_has_scratchbox, has_apt):
        '''Constructor'''

        QtGui.QWizardPage.__init__(self)
        self.setTitle('Install Mode')
        self.setSubTitle(" ")
        intro_label = QtGui.QLabel("Select install mode")

        intro_label.setWordWrap(True)
        self.button_group = QtGui.QButtonGroup()
        self.btn_easy = QtGui.QRadioButton("&Standard installation")
        txt = "Most options well be left for defaults, so there are less question " \
              "asked during installation."
        self.btn_easy.setToolTip(txt)
        self.btn_easy.setWhatsThis(txt)
        self.button_group.addButton(self.btn_easy, 0)
        self.btn_custom = QtGui.QRadioButton("&Custom installation")
        txt = "Allows you to select which parts to install."
        self.btn_custom.setToolTip(txt)
        self.btn_custom.setWhatsThis(txt)
        self.button_group.addButton(self.btn_custom, 1)
        self.btn_uninstall = QtGui.QRadioButton("&Uninstall")
        txt = "Existing Scratchbox installation will be removed from the system."
        self.btn_uninstall.setToolTip(txt)
        self.btn_uninstall.setWhatsThis(txt)
        self.button_group.addButton(self.btn_uninstall, 2)

        if ALLOW_UPGRADE and host_has_scratchbox:
            self.btn_custom.setChecked(True)
        else:
            self.btn_easy.setChecked(True)

        self.layout = QtGui.QVBoxLayout()
        self.layout.addWidget(intro_label)
        self.layout.addWidget(self.btn_easy)
        self.layout.addWidget(self.btn_custom)
        if host_has_scratchbox and has_apt:
            #uninstall currently supported only when installed from debian packages. Not offered otherwise.
            self.layout.addWidget(self.btn_uninstall)
            self.connect(self.btn_uninstall, QtCore.SIGNAL("toggled(bool)"),
                         self.uninstallToggled)
            self.warning = QtGui.QLabel("Warning: %s and %s will be uninstalled "
                                        "from the system. If there are any changes " 
                                        "in your %s home directory, it will be "
                                        "preserved along with the desktop link if "
                                        "there is one. Before going forward, make sure "
                                        "you don't have any active logins." %
                                        (SB_NAME, PRODUCT_NAME, SB_NAME))
            self.warning.setWordWrap(True)
            self.warning.setVisible(False)
            self.layout.addWidget(self.warning)

        self.registerField(FNEasyInstall, self.btn_easy)
        self.registerField(FNUninstall, self.btn_uninstall)
        self.setLayout(self.layout)

    def uninstallToggled(self, on):
        self.warning.setVisible(on)

    def nextId(self):
        '''Determines which page should be shown next'''
        if self.btn_uninstall.isChecked():
            return PageIdSummary

        return PageIdLicense


class LicensePage(QtGui.QWizardPage):
    '''EULA page'''

    def __init__(self, has_64bit):
        '''Constructor'''

        QtGui.QWizardPage.__init__(self)

        self.setTitle('Open Source License Agreement')
        self.setSubTitle(" ")
        self.has_64bit = has_64bit

        license_text_edit = QtGui.QTextEdit()
        license_text_edit.setPlainText(SDK_LICENSE)
        license_text_edit.setReadOnly(True)

        txt = "%s License. Use Ctrl+Wheel to zoom the text." % \
            (PRODUCT_NAME_SHORT)
        license_text_edit.setToolTip(txt)
        license_text_edit.setWhatsThis(txt)

        accept_check_box = QtGui.QCheckBox("I &accept")
        accept_check_box.setCheckState(QtCore.Qt.Unchecked)

        self.registerField(FNLicenseAccept + '*', accept_check_box)

        layout = QtGui.QVBoxLayout()
        layout.addWidget(license_text_edit)
        layout.addWidget(accept_check_box)
        self.setLayout(layout)


    def nextId(self):
        '''Determines which page should be shown next'''
        if self.field(FNEasyInstall).toBool():
            # If couldn't figure out default username, ask user.
            if not get_default_username():
                return PageIdUsers

            return PageIdNokiaBins

        #Custom install
        return PageIdUsers


class InstallOptsPage(QtGui.QWizardPage):
    '''Installation options page'''

    def __init__(self, host_has_scratchbox, scratchbox_op_name, has_xephyr, has_supported_package_manager):
        '''Constructor'''

        QtGui.QWizardPage.__init__(self)
        self.setTitle('Install Options')
        self.setSubTitle(" ")
        self.has_supported_package_manager = has_supported_package_manager
        self.has_scratchbox = host_has_scratchbox
        self.install_sb_checkbox = QtGui.QCheckBox(
            scratchbox_op_name + " &" + SB_NAME)
        self.install_sb_checkbox.setChecked(True)

        if host_has_scratchbox:
            info_str = ("A previous %s installation has been detected "
                        "in %s. You have an option to upgrade that "
                        "installation." % (SB_NAME, SB_PATH))
        else:
            info_str = ("Previous installation of %(sb_name)s was not "
                        "detected. This installer can only detect previous "
                        "installation of %(sb_name)s in %(sb_path)s. If you "
                        "have previously installed %(sb_name)s in some other "
                        "path, new version of %(sb_name)s will be installed "
                        "in %(sb_path)s by this installer." %
                        { "sb_name" : SB_NAME, "sb_path" : SB_PATH})

        info_label = QtGui.QLabel(info_str)
        info_label.setWordWrap(True)

        self.install_sdk_checkbox = QtGui.QCheckBox(
            "Install &%s" % PRODUCT_NAME)
        self.install_sdk_checkbox.setChecked(True)
        self.upgrade_sdk_checkbox = QtGui.QCheckBox(
            "Upgrade &%s" % PRODUCT_NAME)
        self.upgrade_sdk_checkbox.setChecked(False)
        self.upgrade_sdk_checkbox.hide()

        self.registerField(FNInstallSB, self.install_sb_checkbox)
        self.registerField(FNInstallSDK, self.install_sdk_checkbox)
        self.registerField(FNUpgradeSDK, self.upgrade_sdk_checkbox)

        self.connect(self.install_sb_checkbox, QtCore.SIGNAL("toggled(bool)"),
                     self, QtCore.SIGNAL("completeChanged()"))

        self.connect(self.install_sdk_checkbox, QtCore.SIGNAL("toggled(bool)"),
                     self.sdkInstallCheckboxToggled)

        self.layout = QtGui.QVBoxLayout()
        self.layout.addWidget(info_label)
        self.layout.addWidget(self.install_sb_checkbox)
        self.layout.addWidget(self.install_sdk_checkbox)

        self.xephyr_install_checkbox = QtGui.QCheckBox("Install Xephyr")
        self.xephyr_desktop_checkbox = QtGui.QCheckBox("Add Xephyr desktop shortcut")
        self.xephyr_desktop_checkbox.setChecked(True)

        if not has_xephyr:
            if self.has_supported_package_manager:
                self.xephyr_install_checkbox.setChecked(True)
                self.layout.addWidget(self.xephyr_install_checkbox)
        else:
            self.xephyr_install_checkbox.setChecked(False)

        self.layout.addWidget(self.xephyr_desktop_checkbox)

        self.sb_home_shortcut_checkbox = QtGui.QCheckBox("Add shortcut to Scratchbox home directory")
        self.sb_home_shortcut_checkbox.setChecked(True)
        self.layout.addWidget(self.sb_home_shortcut_checkbox)

        self.desktop_links_checkbox = QtGui.QCheckBox("Add Maemo related links to Desktop")
        self.desktop_links_checkbox.setChecked(True)
        self.layout.addWidget(self.desktop_links_checkbox)

        self.registerField(FNCreateSbHomeShortcut, self.sb_home_shortcut_checkbox)

        self.registerField(FNInstallXephyr, self.xephyr_install_checkbox)
        self.registerField(FNInstallXephyrShortcut, self.xephyr_desktop_checkbox)
        self.registerField(FNInstallDesktopLinks, self.desktop_links_checkbox)

        self.connect(self.xephyr_install_checkbox, QtCore.SIGNAL("toggled(bool)"),
                     self.installXephyrCheckboxToggled)
        self.connect(self.xephyr_desktop_checkbox, QtCore.SIGNAL("toggled(bool)"),
                     self, QtCore.SIGNAL("completeChanged()"))
        self.connect(self.sb_home_shortcut_checkbox, QtCore.SIGNAL("toggled(bool)"),
                     self, QtCore.SIGNAL("completeChanged()"))
        self.connect(self.desktop_links_checkbox, QtCore.SIGNAL("toggled(bool)"),
                     self, QtCore.SIGNAL("completeChanged()"))

        self.setLayout(self.layout)


    def installXephyrCheckboxToggled(self, on):
        if on:
            self.xephyr_desktop_checkbox.setDisabled(False)

        else:
            self.xephyr_desktop_checkbox.setChecked(False)
            self.xephyr_desktop_checkbox.setDisabled(True)
        self.emit(QtCore.SIGNAL("completeChanged()"))


    def initializePage(self):
        '''Create list of detected upgradeable SDK targets'''
        targets = []
        if ALLOW_UPGRADE and self.has_scratchbox:
            self.sdk_upgrade = QtGui.QWidget()
            self.sdk_upgrade_layout = QtGui.QVBoxLayout(self.sdk_upgrade)
            self.sdk_upgrade.setLayout(self.sdk_upgrade_layout)
            self.sdk_upgrade.setDisabled(True)
            self.do_sdk_upgrade = False
            user = self.field(FNSelectedUsername).toString()
            targets = self.getTargets(user)
            if targets != []:
                self.install_sdk_checkbox.setChecked(False)
                info_str = ("Following scratchbox targets were detected. "
                            "Check those you want to upgrade. You cannot perform upgrade if doing SDK installation.")

                info_label = QtGui.QLabel(info_str, self.sdk_upgrade)
                info_label.setWordWrap(True)
                self.sdk_upgrade_layout.addWidget(info_label)

                for target in targets:
                    checkbox = QtGui.QCheckBox(target, self.sdk_upgrade)
                    self.sdk_upgrade_layout.addWidget(checkbox)
                    if self.isTargetUpgradeable(user, target):
                        self.connect(checkbox, QtCore.SIGNAL("toggled(bool)"), self, QtCore.SIGNAL("completeChanged()"))
                        checkbox.setChecked(True)
                    else:
                        checkbox.setDisabled(True)

            else:
                info_str = ("No upgradeable scratchbox targets detected")

                info_label = QtGui.QLabel(info_str, self.sdk_upgrade)
                info_label.setWordWrap(True)
                self.sdk_upgrade_layout.addWidget(info_label)

            self.layout.addWidget(self.sdk_upgrade)


    def cleanupPage(self):
        '''Remove current checkboxes if user presses back. We recreate new list with new user selected on previous page'''            
        if ALLOW_UPGRADE and self.has_scratchbox:
            self.sdk_upgrade.deleteLater()


    def sdkInstallCheckboxToggled(self, on):
        if ALLOW_UPGRADE and self.has_scratchbox:
            self.sdk_upgrade.setDisabled(on)
        self.do_sdk_upgrade = not on
        self.emit(QtCore.SIGNAL("completeChanged()"))


    def isTargetUpgradeable(self, user, target):
        sources = SB_PATH + '/users/' + user + '/targets/' + target + '/etc/apt/sources.list'
        try:
            sourcesfile = file(sources,'r')

        except:
            return False

        lines = sourcesfile.readlines()
        sourcesfile.close()
        expression = re.compile(r" fremantle/")
        for line in lines:
            if expression.search(line):
                return True

        return False


    def getTargets(self, user):
        directory='%s/users/' % SB_PATH + user +'/targets'
        dirs = [] #list of directories
        if os.path.exists(directory):
            """Returns a list of directories."""
            #list of directories and files
            listing = os.listdir(directory)

            #get just the directories
            for direntry in listing:
                if os.path.isdir(directory+os.sep+direntry) and direntry != 'links':
                    dirs.append(direntry)
        return dirs


    def validatePage(self):
        if self.install_sdk_checkbox.isChecked():
            #Install selected, not doing any upgrade
            self.upgrade_sdk_checkbox.setChecked(False)
            return True

        if not self.has_scratchbox:
            if self.install_sb_checkbox.isChecked():
                return True

        if ALLOW_UPGRADE and self.do_sdk_upgrade:
            self.do_sdk_upgrade = False
            user = self.field(FNSelectedUsername).toString()
            pwd_ent = pwd.getpwnam(str(user))
            upgradescript = '#!/bin/sh\n'
            upgradescript +='# Automatically generated shell script to upgrade Nokia Maemo5 SDK targets\n'
            upgradescript_fn = UPDATE_SCRIPT_FN
            skipitems = 2
            for child in self.sdk_upgrade.children():
                if skipitems:
                    skipitems -= 1
                else:
                    if child.isChecked():
                        #Target selected for upgrade, append it to our upgrade script
                        #create sources.list file for target, that contains only nokia repos
                        targetname = child.text()
                        sources = SB_PATH + '/users/' + user + '/targets/' + targetname + '/etc/apt/sources.list'
                        try:
                            sourcesfile = file(sources,'r')

                        except:
                            #Couldn't open file, skip to next selected target
                            continue

                        self.do_sdk_upgrade = True
                        lines = sourcesfile.readlines()
                        sourcesfile.close()
                        newsources_fn = '/tmp/maemo-sdk-install-wizard_' + targetname + '_sources.list'
                        expression = re.compile(r" fremantle/")
                        newsourcesfile = file(newsources_fn, 'wt')
                        newsourcesfile.write('# Automatically created by Nokia fremantle SDK installer\n')
                        for line in lines:
                            #Only add Nokia repositories to sources.list
                            if expression.search(line):
                                newsourcesfile.write(line)

                        newsourcesfile.write('\n')
                        newsourcesfile.close()
                        os.chmod(newsources_fn, 0777)
                        os.chown(newsources_fn, pwd_ent.pw_uid, pwd_ent.pw_gid)

                        self.do_sdk_upgrade = True
                        upgradescript += '\n# Upgrade target %s\n' % targetname
                        upgradescript += '%s/tools/bin/sb-conf select %s\n' % (SB_PATH, targetname)
                        upgradescript += 'if [ "$?" -ne "0" ]; then \n' \
                                         '    exit 1\n' \
                                         'fi\n'

                        upgradescript += '%s/login fakeroot apt-get -o Dir::Etc::sourcelist="/tmp/maemo-sdk-install-wizard_%s_sources.list" update\n' % (SB_PATH, targetname)
                        upgradescript += 'if [ "$?" -ne "0" ]; then \n' \
                                         '    exit 1\n' \
                                         'fi\n'
                        upgradescript += '%s/login fakeroot apt-get --force-yes -y install %s\n' % (SB_PATH, UPDATE_INSTALL_PACKAGES)
                        upgradescript += '%s/login fakeroot apt-get -o Dir::Etc::sourcelist="/tmp/maemo-sdk-install-wizard_%s_sources.list -o Acquire::Retries=5" ' \
                                         '--force-yes -y dist-upgrade\n' % (SB_PATH, targetname)
                        upgradescript += 'if [ "$?" -ne "0" ]; then \n' \
                                         '    exit 1\n' \
                                         'fi\n'
                        upgradescript += '%s/login fakeroot apt-get update\n\n' % (SB_PATH)
                        upgradescript += 'rm -f /tmp/maemo-sdk-install-wizard_%s_sources.list\n' % (targetname)

            self.upgrade_sdk_checkbox.setChecked(self.do_sdk_upgrade)
            if self.do_sdk_upgrade:
                #At least one target set to be upgraded, create upgrade script
                upgradescriptfile = file(upgradescript_fn, 'wt')
                upgradescriptfile.write(upgradescript)
                upgradescriptfile.write('rm %s\n' % upgradescript_fn)
                upgradescriptfile.close()
                os.chmod(upgradescript_fn, 0777)
                os.chown(upgradescript_fn, pwd_ent.pw_uid, pwd_ent.pw_gid)
                return True
        if self.install_sb_checkbox.isChecked():
            return True

        return False


    def isComplete(self):
        '''Overrides the method of QWizardPage, to disable next button if
        nothing is selected for installation.'''
        if not self.has_scratchbox and not self.install_sb_checkbox.isChecked():
            return False

        if self.xephyr_install_checkbox.isChecked() or \
           self.xephyr_desktop_checkbox.isChecked() or \
           self.install_sb_checkbox.isChecked() or \
           self.install_sdk_checkbox.isChecked() or \
           self.sb_home_shortcut_checkbox.isChecked() or \
           self.desktop_links_checkbox.isChecked():
            return True

        if self.has_scratchbox and ALLOW_UPGRADE:
            #SDK upgrade selected
            skipitems = 2
            for child in self.sdk_upgrade.children():
                # Skip non-checkbox items
                if skipitems:
                    skipitems -= 1
                else:
                   if child.isChecked():
                       return True

        return False

    def nextId(self):
        '''Determines which page should be shown next'''
        if self.field(FNUpgradeSDK).toBool():
            #sources.list already correct, skip to summary
            return PageIdSummary

        if self.field(FNInstallSDK).toBool():
            if self.field(FNTargetX86Exist).toBool() or self.field(FNTargetArmelExist).toBool():
                return PageIdTargets

            return PageIdPkg
        #No SDK related operations, skip to summary page
        return PageIdSummary

class UsersPage(QtGui.QWizardPage):
    '''User selection page'''

    def __init__(self, has_64bit):
        '''Constructor'''

        QtGui.QWizardPage.__init__(self)
        self.setTitle('User Selection')
        self.setSubTitle(" ")

        self.has_64bit = has_64bit
        all_usernames = get_all_usernames()
        default_username = get_default_username()

        if not default_username: # in case default not found use first one
            default_username = all_usernames[0]

        users_list_widget = QtGui.QListWidget()

        txt = ("List of users on the system. %s will be installed for the "
               "selected user." % PRODUCT_NAME)
        users_list_widget.setToolTip(txt)
        users_list_widget.setWhatsThis(txt)

        # will be set in following signal handler
        self.__selected_username = None
        self.__target_x86_exist = False # targets for selected user
        self.__target_armel_exist = False

        self.connect(users_list_widget,
                     QtCore.SIGNAL("currentTextChanged(const QString &)"),
                     self.selectedUsernameChanged)

        # add usernames to the list
        for index, username in enumerate(all_usernames):
            users_list_widget.addItem(username)

            if username == default_username:
                users_list_widget.setCurrentRow(index)

        self.registerField(FNSelectedUsername, self, "selectedUsername")
        self.registerField(FNTargetX86Exist, self, "targetX86Exist")
        self.registerField(FNTargetArmelExist, self, "targetArmelExist")

        layout = QtGui.QVBoxLayout()
        label = QtGui.QLabel("Install the SDK for the following user:")
        layout.addWidget(label)
        layout.addWidget(users_list_widget)
        self.setLayout(layout)


    def getSelectedUsername(self):
        '''Returns selected username'''
        return self.__selected_username

    selectedUsername = QtCore.pyqtProperty("QString", getSelectedUsername)

    targetX86Exist = QtCore.pyqtProperty(
        "bool", lambda self: self.__target_x86_exist)
    targetArmelExist = QtCore.pyqtProperty(
        "bool", lambda self: self.__target_armel_exist)

    def selectedUsernameChanged(self, new_name):
        '''Signal handler for list widgets currentTextChanged. Sets the
        selected username to the current selection. Also updates related
        info.'''

        # segmentation faults without cast
        self.__selected_username = QtCore.QString(new_name)

        self.__target_x86_exist = scratchbox_target_exists(
            self.__selected_username, TARGET_X86)
        self.__target_armel_exist = scratchbox_target_exists(
            self.__selected_username, TARGET_ARMEL)


    def nextId(self):
        if self.field(FNEasyInstall).toBool():
            return PageIdNokiaBins

        #Custom install
        return PageIdInstallOpts



class TargetsPage(QtGui.QWizardPage):
    '''Targets page, shown only if targets exist for selected user.'''

    def __init__(self):
        '''Constructor'''
        QtGui.QWizardPage.__init__(self)

        self.setTitle('Installation Targets')
        self.setSubTitle(" ")

        self.button_group = QtGui.QButtonGroup()
        self.btn_remove = QtGui.QRadioButton("&Overwrite existing targets")
        self.button_group.addButton(self.btn_remove, 0)
        self.btn_new = QtGui.QRadioButton("Create &new targets")
        self.button_group.addButton(self.btn_new, 1)

        self.group_box = QtGui.QGroupBox(
            "To &create new targets using different name prefix:")

        info_str = ("Specify a prefix to be used for the new targets. "
                    "Ensure that it is not the same as any of the existing "
                    "%s targets (eg: %s). When the new targets are created, "
                    "the architecture (X86 or ARMEL) will be automatically added to the prefix." % \
                    (SB_NAME, TARGET_PREFIX))

        info_label = QtGui.QLabel(info_str)
        info_label.setWordWrap(True)

        self.target_prefix_line_edit = QtGui.QLineEdit()

        group_box_layout = QtGui.QVBoxLayout()
        group_box_layout.addStretch()
        group_box_layout.addWidget(info_label)
        group_box_layout.addWidget(self.target_prefix_line_edit)

        self.group_box.setLayout(group_box_layout)

        self.connect(self.btn_remove,
                     QtCore.SIGNAL("toggled(bool)"),
                     self.removeTargetsCheckboxToggled)

        self.btn_new.setChecked(True)
        self.registerField(FNRemoveTargets, self.btn_remove)
        self.registerField(FNTargetPrefix, self.target_prefix_line_edit)

        layout = QtGui.QVBoxLayout()
        self.label = QtGui.QLabel()
        self.label.setWordWrap(True)
        layout.addWidget(self.label)
        layout.addWidget(self.btn_remove)
        layout.addWidget(self.btn_new)
        layout.addStretch()
        layout.addWidget(self.group_box)
        self.setLayout(layout)

    def initializePage(self):
        '''Overrides the method of QWizardPage, to initialize the page with
        some dynamic text.'''

        target_x86_exist = self.field(FNTargetX86Exist).toBool()
        target_armel_exist = self.field(FNTargetArmelExist).toBool()
        selected_username = self.field(FNSelectedUsername).toString()

        assert target_armel_exist or target_x86_exist, \
            "At least one target should exist for this page to be usable!"

        # both exist
        if target_x86_exist and target_armel_exist:
            targets_str = "The targets %s and %s already exist for user %s." % \
                (TARGET_X86, TARGET_ARMEL, selected_username)

        # either one exist
        else:
            if target_x86_exist:
                targets_str = "The target %s" % TARGET_X86
            elif target_armel_exist:
                targets_str = "The target %s" % TARGET_ARMEL
            targets_str += " already exists for user %s." % selected_username

        self.label.setText("%s Please choose appropriate action." %
                           (targets_str))

        # set the line edits text to some non-existing target prefix
        nonexistent_prefix = TARGET_PREFIX + time.strftime("_%Y%m%d")
        while True:
            if scratchbox_prefix_exist(selected_username, nonexistent_prefix):
                nonexistent_prefix += "_"
            else:
                break
        
        self.target_prefix_line_edit.setText(nonexistent_prefix)


    def validatePage(self):
        '''Overrides the method of QWizardPage, verify valid input from the
        user'''

        # not removing targets, verify that prefix is not used in sb
        if not self.btn_remove.isChecked():
            selected_username = self.field(FNSelectedUsername).toString()

            # prefix exist
            if scratchbox_prefix_exist(selected_username,
                                       self.target_prefix_line_edit.text()):
                QtGui.QMessageBox.critical(
                    self,
                    "Target exists!",
                    "A %s target with the prefix %s already exists for user %s. "
                    "Please specify a new prefix." %
                    (SB_NAME, self.target_prefix_line_edit.text(),
                     selected_username))
                return False

        return True


    def removeTargetsCheckboxToggled(self, on):
        '''Handler for toggled signal of remove_targets_checkbox . Enables
        target name prefix group box if checkbox is unchecked, and disables the
        group box otherwise. When the group box is enabled the focus is set to
        line edit'''
        self.group_box.setDisabled(on)

        if not on: # group box is enabled
            self.target_prefix_line_edit.setFocus()


class PkgPage(QtGui.QWizardPage):
    '''SDK package selection page'''

    def __init__(self):
        '''Constructor'''

        QtGui.QWizardPage.__init__(self)

        self.setTitle('Environment Selection')
        self.setSubTitle(" ")

        self.button_group = QtGui.QButtonGroup()
        self.registerField(FNSDKInstMOptArg, self, "mOptArgument")
        self.registerField(FNSDKInstMOptArgText, self, "mOptArgumentText")

        layout = QtGui.QVBoxLayout()
        label = QtGui.QLabel("Select the environment you wish to install.")
        layout.addWidget(label)
        # add all of the buttons
        for index, item in enumerate(INSTALL_OPTIONS):

            btn = QtGui.QRadioButton(item[0])

            layout.addWidget(btn)
            self.button_group.addButton(btn, index)

            if index == DEFAULT_INSTALL_OPTION: # check the default button
                btn.setChecked(True)

        self.setLayout(layout)

    def getMOptArgument(self):
        '''Returns the argument of the -m option to be used with the SDK
        installer according to current selection'''
        return INSTALL_OPTIONS[self.button_group.checkedId()][1]

    def getMOptArgumentText(self):
        '''Returns the description of the -m option, with mnemonics removed'''
        return INSTALL_OPTIONS[self.button_group.checkedId()][0].replace('&', '')

    mOptArgument = QtCore.pyqtProperty(
        "QString", getMOptArgument,
        doc = "Argument to be passed to the -m option of the SDK installer")

    mOptArgumentText = QtCore.pyqtProperty(
        "QString", getMOptArgumentText,
        doc = "Description (UI text) of argument to be passed to the -m "
        "option of the SDK installer")

    def nextId(self):
        '''Determines which page should be shown next'''
        if self.field(FNSDKInstMOptArg).toString() != INSTALL_OPTIONS[0][1]:
            return PageIdNokiaBins

        return PageIdSummary


class NokiaBinsPage(QtGui.QWizardPage):
    '''Nokia Binaries page'''

    def __init__(self):
        '''Constructor'''
        QtGui.QWizardPage.__init__(self)

        self.setTitle('Nokia Binaries')

        label = QtGui.QLabel("Some of the Maemo APIs and Nokia applications are provided "
                             "by Nokia proprietary binary packages. In order to install them, "
                             "it is required to accept the End User License Agreement (EULA).")
        label.setWordWrap(True)

        self.setSubTitle(" ")
        # Checkbox to toggle installation of Nokia Bins. Will also toggle visibility of
        # components that require it.
        self.nokia_bins_checkbox = QtGui.QCheckBox("Install Nokia &Bins")
        txt = "Install Nokia Bins. Required for Maemo development."
        self.nokia_bins_checkbox.setToolTip(txt)
        self.nokia_bins_checkbox.setWhatsThis(txt)

        # Contains all items that will be hidden when nokia bins checkbox is unchecked
        self.nokia_bins_options = QtGui.QWidget()
        nokia_bins_options_layout = QtGui.QVBoxLayout()

        spacer = QtGui.QSpacerItem(0, 0, QtGui.QSizePolicy.Minimum,
                                         QtGui.QSizePolicy.Expanding)

        self.nokia_apps_checkbox = QtGui.QCheckBox(
            "Install Nokia &Apps")
        txt = "Install Nokia Apps. Requires Nokia Binaries."
        self.nokia_apps_checkbox.setToolTip(txt)
        self.nokia_apps_checkbox.setWhatsThis(txt)
        self.nokia_apps_checkbox.setChecked(True)
        self.registerField(FNInstallNokiaApps, self.nokia_apps_checkbox)

        self.loadinglabel = QtGui.QLabel("Loading EULA page, this may take a while.")
        self.loadinglabel.setWordWrap(True)
        self.loadingprogress = QtGui.QProgressBar()
        self.loadingprogress.setMinimum(0)
        self.loadingprogress.setMaximum(0)

        info_str = ('Please copy the Nokia Binaries sources.list entry from page below. '
                    'Then paste that entry into the edit box below and press confirm button')

        self.info_label = QtGui.QLabel(info_str)
        self.info_label.setWordWrap(True)
        self.info_label.setOpenExternalLinks(True)
        self.info_label.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse or
                                                QtCore.Qt.LinksAccessibleByKeyboard)
        self.info_label.hide()

        self.timeout_timer = QtCore.QTimer()
        self.timeout_timer.setSingleShot(True)

        self.connect(self.timeout_timer, QtCore.SIGNAL("timeout()"), self.pageLoadTimeout)
        # Create line edit widget for repository in case, automatic copying
        # doesn't work on current PyQt version.
        self.repo_line = QtGui.QLineEdit()
        self.repo_line.setReadOnly(True)
        self.repo_line.hide()
        self.web = QtWebKit.QWebView()
        self.webpage = QtWebKit.QWebPage()

        self.confirm_button = QtGui.QPushButton("confirm")
        self.confirm_button.hide()
        self.connect(self.confirm_button,
                     QtCore.SIGNAL("clicked()"),
                     self, QtCore.SIGNAL("completeChanged()"))

        self.webpage.setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks)
        self.web.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding);
        self.connect(self.webpage,
                     QtCore.SIGNAL("loadProgress(int)"),
                     self.loadProgress)
        self.connect(self.webpage.mainFrame(),
                     QtCore.SIGNAL("urlChanged(QUrl)"),
                     self.urlChanged)
        self.connect(self.webpage,
                     QtCore.SIGNAL("loadFinished(bool)"),
                     self.pageLoaded)

        nokia_bins_options_layout.addWidget(self.nokia_apps_checkbox)
        nokia_bins_options_layout.addWidget(self.loadinglabel)
        nokia_bins_options_layout.addWidget(self.loadingprogress)
        nokia_bins_options_layout.addItem(spacer)
        nokia_bins_options_layout.addWidget(self.info_label)
        nokia_bins_options_layout.addWidget(self.web)
        nokia_bins_options_layout.addWidget(self.repo_line)
        nokia_bins_options_layout.addWidget(self.confirm_button)

        self.nokia_bins_options.setLayout(nokia_bins_options_layout)
        self.nokia_bins_checkbox.setChecked(True)

        self.connect(self.nokia_bins_checkbox,
                     QtCore.SIGNAL("toggled(bool)"),
                     self.groupBoxToggled)
        self.connect(self.nokia_bins_checkbox, QtCore.SIGNAL("toggled(bool)"),
                     self, QtCore.SIGNAL("completeChanged()"))

        self.registerField(FNInstallNokiaBins, self.nokia_bins_checkbox)
        self.registerField(FNNokiaBinsRepo, self.repo_line)

        layout = QtGui.QVBoxLayout()
        layout.addWidget(label)
        layout.addWidget(self.nokia_bins_checkbox)
        layout.addWidget(self.nokia_bins_options)
        self.setLayout(layout)

    def initializePage(self):
        if not self.isRepositoryValid():
            #Repository is invalid, probably empty. Erase it just in case.
            self.repo_line.setText("")
            proxy = self.getSystemProxySettings()
            if proxy != None:
                self.webpage.networkAccessManager().setProxy(proxy)
            #Show web page only if repository is not valid, otherwise
            #we will get just install nokia bins & apps checkboxes.
            self.loadEulaPage()

    def loadEulaPage(self):
        self.webpage.mainFrame().load(QtCore.QUrl(EULA_URL))
        self.timeout_timer.start(90000)

    def loadProgress(self, progress):
        '''Update progress bar according to load progress'''
        self.loadingprogress.setMaximum(100)
        self.loadingprogress.setValue(progress)

    def urlChanged(self, url):
        '''Decides what to do when url changes'''
        if url.toString() == TOKEN_URL:
            self.web.hide()
            self.connect(self.webpage,
                         QtCore.SIGNAL("loadFinished(bool)"),
                         self.getRepositoryLine)
        else:
            self.web.show()

    def getRepositoryLine(self, ok):
        '''Copies the repository line from the fully loaded web page'''
        if ok:
            if self.web.page().findText("deb "):
                self.web.page().triggerAction(QtWebKit.QWebPage.SelectEndOfLine)
                self.repo_line.setText(self.webpage.selectedText())

                if self.isRepositoryValid():
                    self.emit(QtCore.SIGNAL("completeChanged()"))
                    self.wizard().next()
                    return

            #Failed to get repository line automatically
            self.info_label.show()
            self.web.show()
            self.confirm_button.show()
            self.repo_line.setText("")
            self.repo_line.setReadOnly(False)
            self.repo_line.show()


    def pageLoaded(self, ok):
        '''Page loaded, stop timeout timer'''
        self.timeout_timer.stop()
        if ok:
            #Loaded page successfully, show page & hide loading text
            self.loadinglabel.hide()
            self.loadingprogress.hide()
            self.web.setPage(self.webpage)
        else:
            #Loading failed (or timeout), try reload
            self.loadingprogress.setMinimum(0)
            self.loadingprogress.setMaximum(0)
            self.web.reload()


    def pageLoadTimeout(self):
        '''Executed when page isn't loaded in specified time'''
        self.web.reload()

 
    def groupBoxToggled(self, on):
        '''Handler for group box's toggled signal. The goal is to focus on line
        edit when group box is enabled. The focus is removed from disabled line
        edit because if it has focus it steals all keyboard events, making
        keyboard unusable. Also, if line edit is disabled the focus is moved to
        the group box's checkbox, without this the focus would jump to the Next
        button.'''

        if on: # group box is enabled
            self.nokia_apps_checkbox.show()
            if not self.isRepositoryValid():
                self.web.show()
                self.loadEulaPage()
        else:
            self.web.hide()
            self.nokia_apps_checkbox.hide()

    def cleanupPage(self):
        '''Overridden, not to clean-up the repo text edit as the default
        implementation does.'''
        return
    
    def validatePage(self):
        '''Overrides the method of QWizardPage, verify valid input from the
        user'''
        # make sure the specified repo seems valid
        if self.nokia_bins_checkbox.isChecked():
            if not self.isRepositoryValid():
                QtGui.QMessageBox.critical(
                    self,
                    "Invalid repository!",
                    "Specified repository is invalid, please provide valid "\
                        "one!",
                    QtGui.QMessageBox.Ok)
                return False
        else:
            self.nokia_apps_checkbox.setChecked(False)
        return True

    def isRepositoryValid(self):
        '''make sure the specified repo is close to valid'''
        repo = str(self.repo_line.text())
        pat = r"deb .* nokia-binaries$"
        if re.match(pat, repo):
            return True
        return False

    def getSystemProxySettings(self):
        '''Returns QNetworkProxy for WebKit based on system proxy settings'''
        proxy = get_proxy('http')
        if proxy:
            http_proxy = proxy['http']
            username_password = []
            index = http_proxy.find('@')
            if index != -1:
                # We have username & password
                split_proxy = http_proxy.split('@')
                if len(split_proxy) == 2:
                    # We have username specified
                    if split_proxy[0].lower().startswith('http://'):
                        username_password = split_proxy[0][7:].split(':')
                        host_port_str = split_proxy[1]
                    else:
                        return None
            else:
                if http_proxy.lower().startswith('http://'):
                    host_port_str = http_proxy[7:]
                else:
                    return None

            host_port = host_port_str.split(':')
            if len(host_port) == 2:
                port = host_port[1].rstrip('/ ')
                try:
                    port_num = int(port)
                except:
                    return None
                proxy = QtNetwork.QNetworkProxy()
                proxy.setHostName(host_port[0])
                proxy.setPort(port_num)
                proxy.setType(QtNetwork.QNetworkProxy.HttpProxy)
                if username_password != []:
                    proxy.setUser(username_password[0])
                    proxy.setPassword(username_password[1])
                return proxy

        return None

    def isComplete(self):
        '''Overrides the method of QWizardPage, to disable next button if
        installation is selected, but repository is not valid.'''
        if self.nokia_bins_checkbox.isChecked():
            return self.isRepositoryValid()
        else:
            return True


class NokiaBinsPageNoWebkit(QtGui.QWizardPage):
    '''Nokia Binaries page'''

    def __init__(self):
        '''Constructor'''

        QtGui.QWizardPage.__init__(self)

        self.setTitle('Nokia Binaries')
        self.setSubTitle(" ")

        label = QtGui.QLabel("Some of the Maemo APIs and Nokia applications are provided "
                             "by Nokia proprietary binary packages. In order to install them, "
                             "it is required to accept the End User License Agreement (EULA).")
        label.setWordWrap(True)

        self.nokia_bins_checkbox = QtGui.QCheckBox(
            "Install Nokia &Bins")
        txt = "Install Nokia Bins. Required for Maemo development."
        self.nokia_bins_checkbox.setToolTip(txt)
        self.nokia_bins_checkbox.setWhatsThis(txt)

        # Contains all items that will be hidden when nokia bins checkbox is unchecked
        self.nokia_bins_options = QtGui.QWidget()
        nokia_bins_options_layout = QtGui.QVBoxLayout()

        self.nokia_apps_checkbox = QtGui.QCheckBox("&Install Nokia Apps")
        self.nokia_apps_checkbox.setChecked(True)
        self.registerField(FNInstallNokiaApps, self.nokia_apps_checkbox)

        info_str = ('Please visit this '
        '<a href="%s">webpage</a> to '
        'obtain Nokia Binaries sources.list entry. Then paste that entry into '
        'the edit box below in order for Nokia Binaries to be installed into '
        'targets by this installer' % (EULA_URL,))

        info_label = QtGui.QLabel(info_str)
        info_label.setWordWrap(True)
        info_label.setOpenExternalLinks(True)
        info_label.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse or
                                           QtCore.Qt.LinksAccessibleByKeyboard)

        txt = "Use link to visit %s to get sources.list entry" % EULA_URL
        info_label.setToolTip(txt)
        info_label.setWhatsThis(txt)

        self.repo_line_edit = QtGui.QLineEdit()

        nokia_bins_options_layout.addWidget(self.nokia_apps_checkbox)
        nokia_bins_options_layout.addWidget(info_label)
        nokia_bins_options_layout.addWidget(self.repo_line_edit)
        self.nokia_bins_options.setLayout(nokia_bins_options_layout)

        self.nokia_bins_checkbox.setChecked(True)
        self.connect(self.nokia_bins_checkbox,
                     QtCore.SIGNAL("toggled(bool)"),
                     self.groupBoxToggled)

        self.registerField(FNInstallNokiaBins, self.nokia_bins_checkbox)
        self.registerField(FNNokiaBinsRepo, self.repo_line_edit)

        layout = QtGui.QVBoxLayout()
        layout.addWidget(label)
        layout.addWidget(self.nokia_bins_checkbox)
        layout.addWidget(self.nokia_bins_options)
        layout.addStretch()
        self.setLayout(layout)

    def groupBoxToggled(self, on):
        '''Handler for group box's toggled signal. The goal is to focus on line
        edit when group box is enabled. The focus is removed from disabled line
        edit because if it has focus it steals all keyboard events, making
        keyboard unusable. Also, if line edit is disabled the focus is moved to
        the group box's checkbox, without this the focus would jump to the Next
        button.'''

        if on: # group box is enabled
            self.nokia_bins_options.setFocusProxy(self.repo_line_edit)
            self.nokia_bins_options.show()
        else:
            self.nokia_bins_options.setFocusProxy(None)
            self.nokia_bins_options.hide()

        self.nokia_bins_options.setFocus()

    def cleanupPage(self):
        '''Overridden, not to clean-up the repo text edit as the default
        implementation does.'''
        return

    def validatePage(self):
        '''Overrides the method of QWizardPage, verify valid input from the
        user'''

        # make sure the specified repo is close to valid
        if self.nokia_bins_checkbox.isChecked():
            repo = str(self.repo_line_edit.text())

            pat = r"deb .* nokia-binaries$"
            m = re.match(pat, repo)

            if not m:
                QtGui.QMessageBox.critical(
                    self,
                    "Invalid repository!",
                    "Specified repository is invalid, please provide valid "\
                        "one!",
                    QtGui.QMessageBox.Ok)
                return False
        else:
            self.nokia_apps_checkbox.setChecked(False)

        return True


class SummaryPage(QtGui.QWizardPage):
    '''Summary page'''

    def __init__(self):
        '''Constructor'''

        QtGui.QWizardPage.__init__(self)

        self.setTitle('Summary')
        self.setSubTitle(" ")

        # pages after this will have back button disabled
        self.setCommitPage(True)

        self.summary_label = QtGui.QLabel()
        self.summary_label.setWordWrap(True)
        self.summary_label.setAlignment(QtCore.Qt.AlignLeft |
                                        QtCore.Qt.AlignTop)

        # scroll area is used to add a scrolling capability in case the
        # text in the label becomes too long
        scroll_area = QtGui.QScrollArea(self)
        scroll_area.setWidgetResizable(True)
        scroll_area.setFrameStyle(QtGui.QFrame.NoFrame)
        scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
        scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)

        # widget placed in scroll area
        sa_widget = QtGui.QWidget()

        # layout for the widget
        sa_widget_layout = QtGui.QVBoxLayout(sa_widget)
        sa_widget_layout.addWidget(self.summary_label)

        scroll_area.setWidget(sa_widget)

        # layout for the page
        layout = QtGui.QVBoxLayout()
        layout.addWidget(scroll_area)

        self.warning_label = QtGui.QLabel()
        self.warning_label.setWordWrap(True)
        self.warning_label.setVisible(False)
        layout.addWidget(self.warning_label)
        self.warning_checkbox = QtGui.QCheckBox("&Install anyway")
        self.warning_checkbox.setVisible(False)
        self.registerField(FNForceInstallLowSpace + '*', self.warning_checkbox)
        layout.addWidget(self.warning_checkbox)

        self.setLayout(layout)

    def initializePage(self):
        '''Overrides the method of QWizardPage, to initialize the page with
        summary text.'''

        summary = ""
        space_needed = 0

        # create the summary text
        if self.field(FNUninstall).toBool():
            self.wizard().setButtonText(QtGui.QWizard.CommitButton,
                                        "&Uninstall")
            summary += "<b>Uninstall %s: </b>Yes<br><br>" % SB_NAME
            install_sb = False
            install_sdk = False
        else:
            self.wizard().setButtonText(QtGui.QWizard.CommitButton,
                                        "&Install")
            # scratchbox
            install_sb = self.field(FNInstallSB).toBool()

            if install_sb:
                 summary += "<b>%s %s: </b>%s<br><br>" % \
                    (self.wizard().hostInfo.scratchbox_op_name, SB_NAME,
                     bool_to_yesno(self.field(FNInstallSB).toBool()))

            # user
            summary += "<b>Username: </b>%s<br><br>" % \
                (self.field(FNSelectedUsername).toString())

            # SDK
            install_sdk = self.field(FNInstallSDK).toBool()
            upgrade_sdk = self.field(FNUpgradeSDK).toBool()
            install_desktop_links = self.field(FNInstallDesktopLinks).toBool()
            summary += "<b>Install %s: </b>%s<br><br>" % \
                (PRODUCT_NAME, bool_to_yesno(install_sdk))

            # these are only available if SDK is installed
            if install_sdk:

                # targets
                target_x86_exist = self.field(FNTargetX86Exist).toBool()
                target_armel_exist = self.field(FNTargetArmelExist).toBool()

                if target_x86_exist or target_armel_exist: # they exist

                    # remove targets
                    remove_targets = self.field(FNRemoveTargets).toBool() or self.field(FNEasyInstall).toBool()

                    summary += "<b>Overwrite targets: </b>%s<br><br>" % \
                        (bool_to_yesno(remove_targets))

                    # target name prefix
                    if not remove_targets:
                        summary += "<b>Target name prefix: </b>%s<br><br>" % \
                            (self.field(FNTargetPrefix).toString())
            
                # packages
                summary += "<b>Packages to install: </b>%s<br><br>" % \
                    (self.field(FNSDKInstMOptArgText).toString())

                # Nokia Binaries
                install_nokia_bins = self.field(FNInstallNokiaBins).toBool() and \
                                     self.field(FNSDKInstMOptArg).toString() != INSTALL_OPTIONS[0][1]
 
                summary += "<b>Install Nokia Binaries: </b>%s<br><br>" % \
                    (bool_to_yesno(install_nokia_bins))

                # Nokia Apps
                if install_sdk or upgrade_sdk:
                    install_nokia_apps = self.field(FNInstallNokiaApps).toBool()
                    summary += "<b>Install Nokia Apps: </b>%s<br><br>" % bool_to_yesno(install_nokia_apps and 
                                                                         self.field(FNSDKInstMOptArg).toString() != INSTALL_OPTIONS[0][1])

            if upgrade_sdk:
                summary += "<b>Upgrade %s: </b>%s<br><br>" % \
                    (PRODUCT_NAME, bool_to_yesno(upgrade_sdk))

            if self.field(FNInstallXephyr).toBool():
                summary += "<b>Install Xephyr: </b>Yes<br><br>"

            summary += "<b>Install Xephyr desktop shortcut: </b>%s<br><br>" % \
                    (bool_to_yesno(self.field(FNInstallXephyrShortcut).toBool()))

            summary += "<b>Install Scratchbox home directory shortcut: </b>%s<br><br>" % \
                    (bool_to_yesno(self.field(FNCreateSbHomeShortcut).toBool()))

            summary += "<b>Install Maemo-related Desktop links: </b>%s<br><br>" % \
                    (bool_to_yesno(install_desktop_links))

        # Calculate approximate needed space
        if self.field(FNInstallXephyr).toBool() or \
           self.field(FNInstallXephyrShortcut).toBool() or \
           self.field(FNCreateSbHomeShortcut) or \
           install_desktop_links:
            space_needed += 5
        # Don't add to space needed, if we are installing scratchbox on top of existing installation
        if install_sb and not self.wizard().hostInfo.has_scratchbox:
            space_needed += 2100

        if install_sdk:
            space_needed += 900

        summary += "<b>Estimated size (MB): </b>%d<br><br>" % space_needed

        # yes, I hate manual work
        dont_want_end = "<br><br>"
        if summary.endswith(dont_want_end):
            summary = summary[:-len(dont_want_end)]

        self.summary_label.setText(summary)

        # Check if /scratchbox exists first, as it might be a symlink to another partition
        if os.path.exists(SB_PATH):
            stat = os.statvfs(SB_PATH)
        else:
            # Not found, check space in root, as that's where we are going to create the dir
            stat = os.statvfs('/')

        free_megs = stat.f_bavail*stat.f_bsize/1024/1024
        # Show warning if user has less space than recommended
        if free_megs < space_needed:
            self.warning_label.setText("<b>Warning:</b> %d megabytes of free space detected, "
                                       "while at least %s is suggested for full scratchbox & "
                                       "SDK installation, are you sure you want to continue?<br>"
                                       "Estimate is rounded up, and doesn't take into account "
                                       "any existing installation you might be overwriting." %
                                       (free_megs, space_needed))
            self.warning_label.setVisible(True)
            self.warning_checkbox.setVisible(True)
            self.warning_checkbox.setChecked(False)

        else:
            self.warning_label.setVisible(True)
            self.warning_checkbox.setVisible(False)
            self.warning_checkbox.setChecked(True)



class ExecutorThread(QtCore.QThread):
    '''Thread that does the actual installation. This is not done in the GUI
    thread not to freeze the GUI.

    Because in GUI applications, the main thread (a.k.a. the GUI thread) is the
    only thread that is allowed to perform GUI-related operations, signals are
    sent from this thread to the main thread to give some visual feedback of
    completed operations.'''

    # thread exit status
    ExitStatusNotStarted = "status_not_started"
    ExitStatusOK         = "status_ok"
    ExitStatusAborted    = "status_aborted"
    ExitStatusError      = "status_error"

    def __init__(self, host_info, uninstall, install_scratchbox, install_sdk, upgrade_sdk, install_apps,
                 sdk_inst_m_opt_arg, selected_username, targets_exist,
                 remove_targets, target_prefix,
                 install_nokia_bins, nokia_bins_repo, install_xephyr, install_xephyr_icon,
                 install_sb_home_shortcut, install_desktop_links):
        '''Constructor'''
        QtCore.QThread.__init__(self)
        self.__host_info = host_info
        self.__uninstall = uninstall
        self.__install_scratchbox = install_scratchbox
        self.__install_sdk = install_sdk
        self.__upgrade_sdk = upgrade_sdk
        self.__install_apps = install_apps
        self.__sdk_inst_m_opt_arg = sdk_inst_m_opt_arg
        self.__selected_username = selected_username
        self.__targets_exist = targets_exist
        self.__remove_targets = remove_targets
        self.__target_prefix = target_prefix
        self.__install_nokia_bins = install_nokia_bins and (self.__sdk_inst_m_opt_arg != INSTALL_OPTIONS[0][1])
        self.__nokia_bins_repo = nokia_bins_repo
        self.__install_xephyr = install_xephyr
        self.__install_xephyr_icon = install_xephyr_icon
        self.__install_sb_home_shortcut = install_sb_home_shortcut
        self.__install_desktop_links = install_desktop_links

        # for progress indication (a task can have many ops)
        self.__ops_total_num = self.__calcOpsTotalNum()
        self.__ops_done_num = 0 # number of done so far

        # abort installation after current task is completed
        self.__abort = False
        self.__is_running_last_task = False

        self.__exit_status = self.__class__.ExitStatusNotStarted

        # signals emitted when new installation operation started/ended (the
        # GUI thread uses these signals to update the progress bar and text on
        # a label)
        self.sig_op_started = QtCore.SIGNAL("opStarted")
        self.sig_op_ended = QtCore.SIGNAL("opEnded")

        LOG("Executor created: %s" % self)

    opsTotalNum = property(lambda self: self.__ops_total_num)
    exitStatus = property(lambda self: self.__exit_status)

    def abort(self):
        '''Called by the main thread to stop this thread after current
        installation operation completes. If the thread is running last task,
        the request to abort will be ignored, since the installation will end
        after that task anyway.

        It is possible to have the abort dialog shown in the beginning while
        the executor will proceed to run last task, in that case the user will
        have "Abort later" button visible even though last task is executed.'''

        if not self.isRunningLastTask():
            LOG("Executor accepted request to abort")
            self.__abort = True
        else:
            LOG("Executor ignoring abort request since running last task")

    def isAborting(self):
        '''Whether this thread is about to abort the installation.'''
        return self.__abort

    def isRunningLastTask(self):
        '''Whether this thread is executing last task.'''
        return self.__is_running_last_task

    def __str__(self):
        '''String representation method.'''
        return str(self.__dict__)

    def __calcOpsTotalNum(self):
        '''Returns number of total operations to complete the
        installation. Each task can have many operations.'''
        count = 0

        if self.__uninstall:
            count += 3
        else:

            if self.__install_scratchbox:
                count += 2

            if self.__install_sdk:
                count += 2

            if self.__upgrade_sdk:
                count += 1

            if self.__install_nokia_bins:
                count += 2 # 1 for each target

            if self.__install_xephyr:
                count += 1

            if self.__install_xephyr_icon:
                count += 1

            if self.__install_sb_home_shortcut:
                count += 1

            if self.__install_desktop_links:
                count += 1


        return count


    def __taskInstallScratchbox(self):
        '''Downloads scratchbox installer, installs scratchbox, removes the
        installer'''

        self.__say("Downloading %s installer" % (SB_NAME))
        sb_installer_fn = download_file(SB_INSTALLER_URL)
        if os.path.isfile("%s/etc/scratchbox-version" %(SB_PATH)):
            self.__say("Upgrading %s" % (SB_NAME))
        else:
            self.__say("Installing %s" % (SB_NAME))

        #If using proxy, give it to the command aswell
        proxy = get_proxy('http')
        if proxy:
            proxystring = "http_proxy=%s " % proxy['http']
        else:
            proxystring = ""

        opt = " -u %s " % self.__selected_username

        # upgrade scratchbox - may change between tries
        if os.path.isfile("%s/etc/scratchbox-version" %(SB_PATH)):
            opt += "-U "

        if not self.__host_info.has_apt_get:
            #We need to set this if installing for tgz
            opt += "-s %s " % SB_PATH

        cmd = proxystring + sb_installer_fn + opt

        # Maximum amount of times to retry installation if it fails (except for out of disk space)
        tries = 3

        while tries > 0:
            # Check first we have enough space to attempt installation. Full install will take
            # more than 80MB, but upgrading (or continuing failed installation) might be doable.
            # User has been warned already, if he hasn't got enough space for full installation.
            # Check if /scratchbox exists first, as it might be a symlink to another partition
            if os.path.exists(SB_PATH):
                stat = os.statvfs(SB_PATH)
            else:
                # Not found, check space in root, as that's where we are going to create the dir
                stat = os.statvfs('/')

            free_megs = stat.f_bavail * stat.f_bsize / 1024 / 1024
            # Biggest package is 40MB, make sure we have at least double that free space
            # before we retry installation
            if free_megs < 80:
                LOG("Less than 80MB free space, aborting installation")
                raise Exception("Less than 80MB free space, aborting installation")

            tries -= 1

            returncode = exec_cmd(cmd, return_errorcode = True)
            if returncode == RC_NO_ERROR:
                # Installation successful, go to next step
                break
            elif returncode == RC_OUTOFDISKSPACE:
                # Ran out of disk space, abort installation.
                os.remove(sb_installer_fn)
                # Don't log anything, as there probably is no space left in /tmp
                raise Exception("Installer execution failed")

            # On other error codes, retry installation, if we have tries left
            if tries > 0:
                LOG("%s installation failed trying again (%d tries left)" % (SB_NAME, tries))
            else:
                # Tried too many times, give up.
                LOG("%s installation failed. Giving up." % (SB_NAME))
                os.remove(sb_installer_fn)
                raise Exception("Scratchbox installation failed")


        LOG("Removing %s installer (%s)" % (SB_NAME, sb_installer_fn))
        os.remove(sb_installer_fn)
        #Add user to group, if not already there
        if not has_user_group(self.__selected_username, SB_GROUP):
            add_user_to_group(self.__selected_username, SB_GROUP)


    def __taskInstallSdk(self):
        '''Downloads SDK installer, installs SDK, removes the installer'''
        global OPTIONS

        self.__say("Downloading %s installer" % (PRODUCT_NAME_SHORT))
        sdk_installer_fn = download_file(SDK_INSTALLER_URL,
                                         self.__selected_username)

        # make the installer really non-interactive
        comment_line = "license\n"
        lines = open(sdk_installer_fn).readlines()

        for index, line in enumerate(lines):
            if line == comment_line:
                lines[index] = "# " + line
                LOG("Successfully patched the SDK installer")
                break
        else:
            raise Exception("SDK installer is strange, line %s not found!" %
                            comment_line)

        open(sdk_installer_fn, "w").writelines(lines)

        # do the installation thing
        proxy = get_proxy('http')
        if proxy:
            cmd = "http_proxy=%s " % proxy['http']
        else:
            cmd = ""

        cmd += "%s -d -m %s" % (sdk_installer_fn, self.__sdk_inst_m_opt_arg)


        if self.__targets_exist:
            if self.__remove_targets:
                cmd += " -y"

            else:
                cmd += " -n %s" % self.__target_prefix
        else:
            cmd += " -y"

        if OPTIONS.sourceslistfile:
            cmd += " -a %s" % OPTIONS.sourceslistfile

        self.__say("Installing %s targets" % PRODUCT_NAME_SHORT)

        tries = 3
        while (True):
            try:
                exec_cmd(cmd, username = self.__selected_username, set_sbox_gid = True)

            except:
                tries -= 1
                if tries:
                    LOG("Failed to install SDK, %d tries left" % tries)
                else:
                    LOG("Failed to install SDK")
                    raise Exception("Failed to install SDK")
                pass
            else:
                break

        LOG("Removing %s installer (%s)" % (PRODUCT_NAME_SHORT,
                                            sdk_installer_fn))
        os.remove(sdk_installer_fn)


    def __taskInstallNokiaBins(self):
        '''Installs Nokia Binaries into the targets'''
        # different target prefix was chosen
        if self.__targets_exist and not self.__remove_targets:
            armel_target = self.__target_prefix + TARGET_POSTFIX_ARMEL
            x86_target = self.__target_prefix + TARGET_POSTFIX_X86

        # default target names
        else:
            armel_target = TARGET_ARMEL
            x86_target = TARGET_X86

        for target in [armel_target, x86_target]:

            self.__say("Installing Nokia Binaries into target %s" % (target))
            packages = "nokia-binaries "
            if self.__install_apps:
                packages += "nokia-apps "
            # select target
            LOG("Selecting target %s in scratchbox" % target)
            cmd = "%s/tools/bin/sb-conf select %s" % (SB_PATH, target)
            try:
                exec_cmd(cmd, username = self.__selected_username, set_sbox_gid = True)
            except:
                LOG("Failed to select target %s" % target)
                raise Exception("Failed to select target")

            # add repo to sources.list
            sources_list_fn = (
                "%s/users/%s/targets/%s/etc/apt/sources.list" %
                (SB_PATH, self.__selected_username, target))

            LOG("Adding Nokia Binaries repo (%s) to sources.list of the "
                "target (%s)" % (self.__nokia_bins_repo, sources_list_fn))

            file_append_lines(
                sources_list_fn,
                ["",
                 "# Repository for Nokia Binaries, added by %s" % (MY_NAME),
                 self.__nokia_bins_repo])

            # install the binaries
            LOG("Starting the installation of packages %s" % packages)

            proxy = get_proxy('http')
            if proxy:
                cmd = "http_proxy=%s " % proxy['http']
            else:
                cmd = ""

            cmd += ("%s/login fakeroot apt-get update && %s/login "
                    "fakeroot apt-get -o Acquire::Retries=5 install %s --force-yes -y" %
                    (SB_PATH, SB_PATH, packages))
            LOG("Executing command: %s" % cmd)

            tries = 3
            while (True):
                try:
                    exec_cmd(cmd, username = self.__selected_username, set_sbox_gid = True)
                except:
                    tries -= 1
                    if tries:
                        LOG("Failed to install Nokia Binaries, %d tries left" % tries)
                    else:
                        LOG("Failed to install Nokia Binaries, giving up")
                        raise Exception("Failed to install Nokia Binaries, giving up")
                    pass
                else:
                    break


    def __taskUpgradeSdk(self):
        '''Upgrade selected targets'''
        self.__say("Upgrading SDK targets")

        LOG("Starting to upgrade targets")
        cmd = UPDATE_SCRIPT_FN
        if os.path.exists(cmd):
            try:
                exec_cmd(cmd, username = self.__selected_username, set_sbox_gid = True)
            except:
                LOG("Failed to upgrade SDK, giving up")
                raise Exception("Failed to upgrade SDK, giving up")


    def __taskInstallXephyr(self):
        '''Try to install Xephyr'''
        self.__say("Installing xephyr")
        if install_package('xserver-xephyr', askuser = False):
            # Installation completed successfully, make sure we have the binary in path now
            if not Which('Xephyr'):
                raise Exception("Xephyr binary missing after trying to install Xephyr.")

        else:
            LOG("Failed to install Xephyr.")
            raise Exception("Failed to install Xephyr.")

        return


    def __taskInstallXephyrShortcut(self):
        '''Install Xephyr launcher icon & shell script'''
        self.__say("Installing xephyr desktop shortcut")
        LOG("Starting to install xephyr desktop shortcut")
        desktopentry_fn = XEPHYR_DESKTOP_ENTRY_FILENAME
        scriptpath = None
        for path in BINPATHS:
            if os.path.isdir(path):
                LOG("Using path %s" % path)
                scriptpath = path
                break

        if scriptpath:
            LOG("Installing Xephyr launcher %s to %s" % (XEPHYR_SHORTCUT_FILENAME, scriptpath))
            filename = "%s/%s" % (scriptpath, XEPHYR_SHORTCUT_FILENAME)
            #Newer Xephyr has no -kb argument. Check if installed version supports it.
            if xephyr_has_kb():
                kb_arc = ' -kb'
            else:
                kb_arc = ''

            script = ("#!/bin/sh\n"
                      "# Automatically created by %s\n"
                      "(Xephyr :2 -host-cursor -screen 800x480x16 -dpi 96 -ac%s; newgrp %s <<'END'\n"
                      "%s/login af-sb-init.sh stop\n"
                      "END\n"
                      ") &\n"
                      "newgrp %s <<'END'\n"
                      "sleep 3\n"
                      "%s/login sb-conf select %s\n"
                      "%s/login af-sb-init.sh restart\n"
                      "END\n" % (MY_NAME, kb_arc, SB_GROUP, SB_PATH, SB_GROUP, SB_PATH, TARGET_X86, SB_PATH))

            launcherfile = file(filename, 'wt')
            launcherfile.write(script)
            launcherfile.close()
            p = subprocess.Popen(['chmod', '0755', '%s' % filename])
            p.wait()
            if p.returncode:
                raise Exception("Failed to set file execution permission for %s" % filename)
            p = subprocess.Popen(['chown', 'root:%s' % SB_GROUP, '%s' % filename])
            p.wait()
            if p.returncode:
                raise Exception("Failed to set file owner")

            desktopdir = get_user_desktop_dir(self.__selected_username)
            if desktopdir:
                desktopfilename = "%s/%s" % (desktopdir, desktopentry_fn)
                LOG("Installing Xephyr desktop entry %s to %s" % (desktopentry_fn, desktopdir))
                desktopentry = ("[Desktop Entry]\n"
                                "Version=5.0\n"
                                "Encoding=UTF-8\n"
                                "Name=Maemo5 SDK\n"
                                "Comment=shortcut to maemo desktop\n"
                                "Exec=%s\n"
                                "Terminal=false\n"
                                "Type=Application\n"
                                "StartupNotify=true\n" % filename)
                desktopfile = file(desktopfilename, 'wt')
                desktopfile.write(desktopentry)
                desktopfile.close()
                p = subprocess.Popen(['chmod', '0750', '%s' % desktopfilename])
                p.wait()
                if p.returncode:
                    raise Exception("Failed to set file permissions for %s" % desktopfilename)

                p = subprocess.Popen(['chown', '%s' % (self.__selected_username), '%s' % desktopfilename])
                p.wait()
                if p.returncode:
                    raise Exception("Failed to set file owner for %s" % desktopfilename)

    def __taskInstallSbHomeShortcut(self):
        '''Install symbolic link to Scratchbox home directory to users Desktop dir'''
        self.__say("Adding scratchbox home shortcut")
        LOG("Starting to add scratchbox home shortcut")
        shortcut_fn = "sbhome"

        desktopdir = get_user_desktop_dir(self.__selected_username)

        if desktopdir:
            filename = "%s/%s" % (desktopdir, shortcut_fn)
            if not os.path.islink(filename):
                sbhomedir = "%s/users/%s/home/%s/" % (SB_PATH, self.__selected_username, self.__selected_username)
                if os.path.isdir(sbhomedir):
                    LOG("Adding scratchbox home shortcut %s to %s" % (shortcut_fn, desktopdir))
                    p = subprocess.Popen(['ln', '-s', sbhomedir, filename])
                    p.wait()
                    if p.returncode:
                        raise Exception("Failed to create symbolic link")
                else:
                    LOG("Couldn't find directory %s, not adding link" % (sbhomedir))

    def __taskInstallLinks(self):
        '''Installs html file with maemo-related links to Desktop'''
        self.__say("Adding links to desktop")
        htmldata = ('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">'
                    '<HTML>'
                    '<HEAD>'
                    '	<META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=utf-8">'
                    '	<TITLE></TITLE>'
                    '	<META NAME="GENERATOR" CONTENT="OpenOffice.org 3.0  (Unix)">'
                    '	<META NAME="CREATED" CONTENT="20091113;11272200">'
                    '	<META NAME="CHANGED" CONTENT="20091113;11442500">'
                    '	<STYLE TYPE="text/css">'
                    '	<!--'
                    '		@page { size: 8.27in 11.69in; margin: 1in }'
                    '	-->'
                    '	</STYLE>'
                    '</HEAD>'
                    '<BODY LANG="en" DIR="LTR">'
                    '<P ALIGN=CENTER><BR><BR>'
                    '</P>'
                    '<P ALIGN=CENTER><FONT COLOR="#ff950e"><FONT SIZE=7 STYLE="font-size: 40pt"><B>Maemo 5 </B></FONT></FONT>'
                    '</P>'
                    '<P ALIGN=CENTER><BR><BR>'
                    '</P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in; text-decoration: none"><FONT COLOR="#ff950e"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=5><B>Getting Started</B></FONT></FONT></FONT></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://wiki.maemo.org/Documentation/Maemo_5_Final_SDK"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Introduction</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://wiki.maemo.org/Documentation/Maemo_5_Developer_Guide/Architecture"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Architecture</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://www.forum.nokia.com/info/sw.nokia.com/id/97e9b8e0-904c-4141-bb8a-91d4f519735f/Maemo_5_Desktop_Widget_UI_Guidelines.html"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Desktop Widget UI Guidelines</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://wiki.maemo.org/Documentation/Maemo_5_Developer_Guide"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Developer Guide</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://wiki.maemo.org/Documentation/devtools/maemo5"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Developer Tools</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://maemo.org/development/sdks/maemo_5_api_documentation/"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">API References</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="https://garage.maemo.org/svn/maemoexamples/trunk/"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal"><FONT COLOR="#999999"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Sample Example Code</SPAN></FONT></FONT></FONT></SPAN></FONT></FONT></SPAN></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><BR><BR>'
                    '</P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in; text-decoration: none"><FONT COLOR="#ff950e"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=5><B>More Information</B></FONT></FONT></FONT></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://wiki.maemo.org/Q&amp;A_-_SDK_and_Scratchbox"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Q &amp; A SDK and Scratchbox</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://wiki.maemo.org/Q&amp;A_Porting_to_Fremantle"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Q &amp; A Porting to Fremantle</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://wiki.maemo.org/Accelerometers"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Accelerometers</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://wiki.maemo.org/OpenGL-ES"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">OpenGL-ES</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><BR><BR>'
                    '</P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in; text-decoration: none"><FONT COLOR="#ff950e"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=5><B>Community Application Repositories</B></FONT></FONT></FONT></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://wiki.maemo.org/Extras-devel"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Extras-devel</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://wiki.maemo.org/Extras-testing"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Extras-testing</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '<P ALIGN=LEFT STYLE="margin-left: 0.85in"><A HREF="http://wiki.maemo.org/Uploading_to_Extras"><FONT COLOR="#999999"><SPAN STYLE="text-decoration: none"><FONT FACE="ArialMT, sans-serif"><FONT SIZE=4 STYLE="font-size: 16pt"><SPAN STYLE="font-weight: normal">Uploading packages</SPAN></FONT></FONT></SPAN></FONT></A></P>'
                    '</BODY>'
                    '</HTML>\n')

        desktopdir = get_user_desktop_dir(self.__selected_username)
        if desktopdir:
            LOG("Adding links to %s" % desktopdir)
            filename = '%s/%s' % (desktopdir, MAEMO_LINKS_FILENAME)

            htmlfile = file(filename, 'wt')
            htmlfile.write(htmldata)
            htmlfile.close()
            p = subprocess.Popen(['chmod', '0750', '%s' % filename])
            p.wait()
            if p.returncode:
                raise Exception("Failed to set file permissions for %s" % filename)

            p = subprocess.Popen(['chown', '%s' % (self.__selected_username), '%s' % filename])
            p.wait()
            if p.returncode:
                raise Exception("Failed to set file owner for %s" % filename)


    def __taskUninstallSb(self):
        '''Uninstall scratchbox from system. If user home directory is not removed, the symbolic link is kept aswell.'''
        self.__say("Uninstalling %s targets" % (SB_NAME))
        LOG("Starting to uninstall %s targets" % (SB_NAME))
        desktopdir = get_user_desktop_dir(self.__selected_username)

        #1)Remove targets
        userlist = get_scratchbox_users()
        for user in userlist:
            if has_user_active_sessions(user):
                LOG("User %s has active scratchbox session(s), which prevent "
                    "%s uninstallation." % (SB_NAME, SB_NAME))
                raise Exception ("User %s has active scratchbox session(s)" % user)
            else:
                #User doesn't have active sessions, remove targets
                targets = get_user_targets(user)
                if targets:
                    LOG("Removing targets for user %s" % (user))

                for target in targets:
                    #LOG("Removing target %s" % (target))
                    remove_scratchbox_target(user, target)

        #2)Remove scratchbox
        self.__say("Uninstalling %s" % (SB_NAME))
        LOG("Targets removed, starting to uninstall %s" % (SB_NAME))
        uninstall_scratchbox(self.__host_info.has_64bit)
        #3)Remove scratchbox home directory symlink, if /scratchbox dir was removed
        if not os.path.isdir(SB_PATH):
            shortcut_fn = 'sbhome'
            filename = "%s/%s" % (desktopdir, shortcut_fn)
            if os.path.islink(filename):
                LOG("Removing desktop shortcut to %s, as directory no longer exists" % SB_PATH)
                exec_cmd('rm %s' % filename)

        #4)Remove xephyr launcher.
        self.__say("%s uninstalled, removing Xephyr launcher" % (SB_NAME))
        LOG("%s uninstalled, removing Xephyr launcher" % (SB_NAME))
        scriptpath = None
        for path in BINPATHS:
            if os.path.isdir(path):
                scriptpath = path
                break
        if scriptpath:
            filename = scriptpath + "/" + XEPHYR_SHORTCUT_FILENAME
            if os.path.isfile(filename):
                LOG("Removing Xephyr launcher")
                exec_cmd('rm %s' % filename)


        if desktopdir:
            #5)Remove xephyr launcher desktop icon.
            filename = "%s/%s" % (desktopdir, 'xephyr.desktop')
            if os.path.isfile(filename):
                LOG("Removing Xephyr launcher icon")
                exec_cmd('rm %s' % filename)

            #6)Remove Maemo links.
            filename = '%s/%s' % (desktopdir, MAEMO_LINKS_FILENAME)
            if os.path.isfile(filename):
                LOG("Removing Maemo links")
                exec_cmd('rm "%s"' % filename)


    def __getTasks(self):
        '''Returns list of installation tasks to be executed in order'''

        tasks = []
        #Needed by sb-conf used in uninstall aswell
        if self.__uninstall:
            tasks.append(self.__taskUninstallSb)
        else:

            if self.__install_scratchbox:
                tasks.append(self.__taskInstallScratchbox)

            if self.__install_sdk:
                tasks.append(self.__taskInstallSdk)

            if self.__upgrade_sdk:
                tasks.append(self.__taskUpgradeSdk)

            if self.__install_nokia_bins:
                tasks.append(self.__taskInstallNokiaBins)

            if self.__install_xephyr:
                tasks.append(self.__taskInstallXephyr)

            if self.__install_xephyr_icon:
                tasks.append(self.__taskInstallXephyrShortcut)

            if self.__install_sb_home_shortcut:
                tasks.append(self.__taskInstallSbHomeShortcut)

            if self.__install_desktop_links:
                tasks.append(self.__taskInstallLinks)

        LOG("Got tasks: %s" % tasks)
        return tasks

    def run(self):
        '''Runs the installation'''
        tasks = self.__getTasks()

        try:
            for task in tasks:

                if self.__abort:
                    LOG("Executor aborting")
                    self.__exit_status = self.__class__.ExitStatusAborted
                    break

                else:
                    LOG("Executor starting task %s" % (task))

                    if task == tasks[-1]:
                        self.__is_running_last_task = True
                        LOG("Started task is the last one")
                        
                    task()

            # all tasks successfully completed
            else:
                self.__exit_status = self.__class__.ExitStatusOK
                # for the last op
                self.emit(self.sig_op_ended, self.__ops_done_num)

        except: # CmdExecError, e:
            LOG.log_exc()
            self.__exit_status = self.__class__.ExitStatusError

        LOG("Executor set exit status to (%s)" % (self.__exit_status))


    def __say(self, msg):
        '''Communicates with the main thread about installation progress using
        signals'''

        self.emit(self.sig_op_ended, self.__ops_done_num) # previous op ended

        self.__ops_done_num += 1 # new op started

        assert self.__ops_done_num <= self.__ops_total_num, "Too many ops!"

        msg = "%s/%s %s" % (self.__ops_done_num, self.__ops_total_num, msg)

        LOG("S: %s" % msg)
        self.emit(self.sig_op_started, msg, self.__ops_done_num)


class ProgressPage(QtGui.QWizardPage):
    '''Progress page'''

    def __init__(self, tail_process):
        '''Constructor'''

        QtGui.QWizardPage.__init__(self)

        # whether the slider in the text widget has been scrolled to its
        # vertical scroll bars end, it will be scrolled only once at
        # initialization; when the slider is scrolled to the vertical end, the
        # newly appended text will cause automatic scrolling, so the new text
        # will always be visible
        self.scrolled_to_vend = False

        self.setSubTitle(" ")

        self.status_label = QtGui.QLabel("Installing...")
        self.status_label.setWordWrap(True)

        self.progress_bar = QtGui.QProgressBar()

        self.error_label = QtGui.QLabel("")

        txt = ('Log file <a href="file://%s">%s</a>' %
               (LOG.fn_log, LOG.fn_log))

        self.logs_url_label = QtGui.QLabel(txt)
        self.logs_url_label.setWordWrap(True)
        self.logs_url_label.setOpenExternalLinks(True)
        self.logs_url_label.setTextInteractionFlags(
            QtCore.Qt.TextBrowserInteraction)

        # some systems have such bug that file can't be opened by clicking on
        # URL, so no hint on using the link, see
        # https://bugs.launchpad.net/ubuntu/+source/xdg-utils/+bug/362121

        self.logs_text_edit = QtGui.QTextEdit()

        txt = "Logs from %s.  Use Ctrl+Wheel to zoom the text." % LOG.fn_log
        self.logs_text_edit.setToolTip(txt)
        self.logs_text_edit.setWhatsThis(txt)

        # set proper coloring of the text edit
        # must be done before writing any text, otherwise black text
        # will not be visible once you change background to black
        #
        # new in Qt 4.4: not used now, since some systems won't have it
        # self.logs_text_edit.setTextBackgroundColor(QtCore.Qt.black)
        palette = QtGui.QPalette()
        palette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base,
                         QtCore.Qt.black);
        palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Base,
                         QtCore.Qt.black);
        self.logs_text_edit.setPalette(palette);
        self.logs_text_edit.setTextColor(QtCore.Qt.green)

        # there must be some text in the editor, otherwise the color of text
        # written with cursor will be black, why? go figure...
        self.logs_text_edit.setPlainText("logs from %s\n" % LOG.fn_log)

        self.logs_text_edit.setReadOnly(True)

        # to look like a proper terminal use fixed width fonts
        self.logs_text_edit.setFont(QtGui.QFont("Monospace", 10))

        # cursor is used to append text to the end of the edit widget, there is
        # append method but it adds extra newline, and InsertPlainText method
        # inserts text at current cursor position, which can be changed by the
        # user just by clicking somewhere in the edit widget
        self.cursor = self.logs_text_edit.textCursor()
        self.cursor.movePosition(QtGui.QTextCursor.End)

        self.logs_button = QtGui.QPushButton("&Logs")
        txt = "Toggles visibility of the logs view"
        self.logs_button.setToolTip(txt)
        self.logs_button.setWhatsThis(txt)
        self.logs_button.setCheckable(True)
        self.connect(self.logs_button, QtCore.SIGNAL("toggled(bool)"),
                     self.logsButtonToggled)

        # takes the space of text edit when it is hidden
        self.spacer = QtGui.QSpacerItem(0, 0,
                                        QtGui.QSizePolicy.Minimum,
                                        QtGui.QSizePolicy.Expanding)

        self.connect(tail_process,
                     QtCore.SIGNAL("readyReadStandardOutput()"),
                     self.tailProcessReadStdout)

        layout = QtGui.QVBoxLayout()
        layout.addWidget(self.status_label)
        layout.addWidget(self.progress_bar)
        layout.addWidget(self.error_label)
        # spacer will be removed in button's toggled signal handler
        layout.addItem(self.spacer)
        layout.addWidget(self.logs_url_label)
        layout.addWidget(self.logs_text_edit)
        self.setLayout(layout)

        self.logs_button.setChecked(True) # will emit toggled signal

    def initializePage(self):
        '''Overrides the method of QWizardPage, to initialize the page with
        some default settings.'''

        QtGui.QApplication.setOverrideCursor(QtCore.Qt.BusyCursor)

        if self.field(FNUninstall).toBool():
            self.setTitle('Uninstalling')
            self.executor = ExecutorThread(
                self.wizard().hostInfo,
                True,
                False,
                False, False, False,
                '',
                str(self.field(FNSelectedUsername).toString()),
                False,
                False,
                '',
                False,
                '', 
                False,
                False,
                False,
                False)
        else:
            self.setTitle('Installing')
            target_x86_exist = self.field(FNTargetX86Exist).toBool()
            target_armel_exist = self.field(FNTargetArmelExist).toBool()
            targets_exist = target_x86_exist or target_armel_exist

            if self.field(FNEasyInstall).toBool():
                #Easy install selected, use defaults
                remove_targets = True
                install_sb = True
                install_sdk = True
                upgrade_sdk = False
                install_nokia_bins = True
                install_apps = True
                if Which('Xephyr'):
                    install_xephyr = False
                else:
                    install_xephyr = True

                install_xephyr_shortcut = True
                create_sbhome_shortcut = True
                install_desktop_links = True
                # If default username cannot be figured out, use one selected on User selection page
                selected_username = str(self.field(FNSelectedUsername).toString())
                install_option = INSTALL_OPTIONS[DEFAULT_INSTALL_OPTION][1]

            else:
                remove_targets = self.field(FNRemoveTargets).toBool()
                install_sb = self.field(FNInstallSB).toBool()
                install_sdk = self.field(FNInstallSDK).toBool()
                upgrade_sdk = self.field(FNUpgradeSDK).toBool()
                install_option = str(self.field(FNSDKInstMOptArg).toString())
                #if minimal rootstrap, don't install nokia bins
                if install_sdk and install_option != INSTALL_OPTIONS[0][1]:
                    install_nokia_bins = self.field(FNInstallNokiaBins).toBool()
                else:
                    install_nokia_bins = False

                #nokia apps depend on nokia bins
                if install_nokia_bins:
                    install_apps = self.field(FNInstallNokiaApps).toBool()
                else:
                    install_apps = False

                install_xephyr = self.field(FNInstallXephyr).toBool()
                install_xephyr_shortcut = self.field(FNInstallXephyrShortcut).toBool()
                create_sbhome_shortcut = self.field(FNCreateSbHomeShortcut).toBool()
                install_desktop_links = self.field(FNInstallDesktopLinks).toBool()
                selected_username = str(self.field(FNSelectedUsername).toString())

            if remove_targets:
                target_prefix = ""
            else:
                target_prefix = str(self.field(FNTargetPrefix).toString())


            if install_nokia_bins:
                nokia_bins_repo = str(self.field(FNNokiaBinsRepo).toString())
            else:
                nokia_bins_repo = ""

            if remove_targets:
                target_prefix = ""
            else:
                target_prefix = str(self.field(FNTargetPrefix).toString())
            self.executor = ExecutorThread(
                self.wizard().hostInfo,
                False,
                install_sb,
                install_sdk, upgrade_sdk, install_apps,
                install_option,
                selected_username,
                targets_exist,
                targets_exist and remove_targets,
                target_prefix,
                install_nokia_bins,
                nokia_bins_repo, 
                install_xephyr,
                install_xephyr_shortcut,
                create_sbhome_shortcut,
                install_desktop_links)

        self.progress_bar.setRange(0, self.executor.opsTotalNum)
        self.progress_bar.setValue(0) # kinda redundant

        self.connect(self.executor, self.executor.sig_op_started,
                     self.executorOpStarted)

        self.connect(self.executor, self.executor.sig_op_ended,
                     self.executorOpEnded)

        self.connect(self.executor, QtCore.SIGNAL("finished()"),
                     self.executorFinished)

        self.executor.start()

        # have a custom button to show/hide logs
        self.wizard().setOption(QtGui.QWizard.HaveCustomButton1, True)
        self.wizard().setButton(QtGui.QWizard.CustomButton1, self.logs_button)

    def validatePage(self):
        '''Overrides the method of QWizardPage, to remove the custom
        button.'''
        self.wizard().setOption(QtGui.QWizard.HaveCustomButton1, False)

        # QWizard deletes the old button when setButton is used
        self.wizard().setButton(QtGui.QWizard.CustomButton1, None)

        return True

    def isBusy(self):
        '''Returns True if installation/removal is in progress'''
        return self.executor.isRunning()

    def executorOpStarted(self, msg, op_num): 
        '''Called when the thread has started executing an operation'''
        self.status_label.setText(msg)

    def executorOpEnded(self, op_num):
        '''Called when the thread has ended executing an operation'''
        self.progress_bar.setValue(op_num)

    def executorFinished(self):
        '''Called when executor thread has finished its jobs. This means
        installation or removal is complete at this point. So this routine
        reflects that to the UI'''
        if self.field(FNUninstall).toBool():
            self.setTitle('Uninstallation Process Completed')
        else:
            self.setTitle('Installation Process Completed')

        # error
        if self.executor.exitStatus == ExecutorThread.ExitStatusError:
            if self.field(FNUninstall).toBool():
                self.error_label.setText("Uninstallation was aborted by fatal error.")
            else:
                self.error_label.setText("Installation was aborted by fatal error.")

            # show the failed op the label
            self.status_label.setText(
                "<font color=red><b>Failed: %s</font></b>" %
                self.status_label.text())

        # abort
        elif self.executor.exitStatus == ExecutorThread.ExitStatusAborted:
            if self.field(FNUninstall).toBool():
                self.error_label.setText("Uninstallation was aborted by the user.")
            else:
                self.error_label.setText("Installation was aborted by the user.")
            self.status_label.setText("Aborted")

        # ok
        elif self.executor.exitStatus == ExecutorThread.ExitStatusOK:
            if self.field(FNUninstall).toBool():
                self.error_label.setText("Uninstallation completed successfully.")
            else:
                self.error_label.setText("Installation completed successfully.")
            self.status_label.setText("Done")

        else:
            assert False, "Illegal thread exit status (%s)!" % \
                (self.executor.exitStatus)

        # enable the Next/Finish button
        self.emit(QtCore.SIGNAL("completeChanged()"))

        # Finish button will replace Next button if installation failed
        # setFinalPage works with Qt versions 4.5 and higher, see the bug:
        # http://www.qtsoftware.com/developer/task-tracker/index_html?method=entry&id=222140
        if self.isLastPage():
            if QtCore.QT_VERSION >= 0x040500:
                self.setFinalPage(True)

            else:
                finish_btn = self.wizard().button(QtGui.QWizard.FinishButton)
                finish_btn.setVisible(True)
                finish_btn.setEnabled(True)
                finish_btn.setDefault(True)
                
                next_btn = self.wizard().button(QtGui.QWizard.NextButton)
                next_btn.setVisible(False)

        # from this page on there is nothing to cancel, so disabled
        self.wizard().button(QtGui.QWizard.CancelButton).setEnabled(False)

        QtGui.QApplication.restoreOverrideCursor()

    def isLastPage(self):
        '''Returns True if this page is the last one to show (in which case
        Finish button will replace the Next button). This page will be the last
        if installation fails.'''
        return self.executor.exitStatus in \
            [ExecutorThread.ExitStatusAborted, ExecutorThread.ExitStatusError]

    def nextId(self):
        '''Overrides the method of QWizardPage, not to show last page in case
        installation fails.'''
        # installation failed
        if self.isLastPage():
            return -1

        # installation succeeded
        else:
            return QtGui.QWizardPage.nextId(self)

    def isComplete(self):
        '''Overrides the method of QWizardPage, to disable next button when
        installation/removal is in progress.'''
        if self.isBusy():
            return False
        else:
            return True

    def showAbortMsgBox(self):
        '''Shows abort message box. Returns tuple of buttons texts.'''

        msg_box = QtGui.QMessageBox(
            QtGui.QMessageBox.Question,
            "Really abort?",
            "Installation processes are still running!")

        # have not chosen to abort already in the past and not running last task
        have_abort_later_btn = not self.executor.isAborting() and \
            not self.executor.isRunningLastTask()

        abort_now_btn_txt = "Abort now"
        abort_later_btn_txt = "Abort later"

        abort_now_info_text = (
            "If you choose to '%s' all of the installation processes will be "
            "killed. <b>NOTE!</b> <i>This could potentially make your system "
            "unstable. So use it at your own risk.</i>" % abort_now_btn_txt)

        if have_abort_later_btn:
            abort_later_info_text = (
                "You can abort safely by choosing '%s', in which case "
                "currently running installation process will be allowed to "
                "complete its execution." % (abort_later_btn_txt))

        # don't have the abort later button
        else:
            if self.executor.isRunningLastTask():
                abort_later_info_text = (
                    "It appears that the last installation process is running. "
                    "Hence, installation should be over any minute now! It is "
                    "highly recommended to wait for the completion of "
                    "installation instead of aborting.")

            else:
                abort_later_info_text = (
                    "You have previously selected '%s', so installation will "
                    "be aborted after currently running installation process "
                    "completes its execution." % (abort_later_btn_txt))

        info_txt = (abort_later_info_text + "<br><br>" + abort_now_info_text)

        msg_box.setInformativeText(info_txt)

        cancel_btn = msg_box.addButton(QtGui.QMessageBox.Cancel)
        msg_box.addButton(abort_now_btn_txt, QtGui.QMessageBox.DestructiveRole)

        if have_abort_later_btn:
            msg_box.addButton(abort_later_btn_txt, QtGui.QMessageBox.AcceptRole)

        msg_box.setDefaultButton(cancel_btn)

        msg_box.exec_()

        clicked_btn = msg_box.clickedButton()
        LOG("Answer to abort dialog: %s" % clicked_btn.text())

        # msg_box & clicked_btn will not exist after this function returns
        return (clicked_btn.text(), abort_now_btn_txt, abort_later_btn_txt,
                cancel_btn.text())

    def onCancel(self):
        '''Called if user tries to close or cancel the wizard, shows the abort
        dialog and takes respective actions.'''
        (clicked_btn_txt, abort_now_btn_txt,
         abort_later_btn_txt, cancel_btn_txt) = self.showAbortMsgBox()

        # cannot abort if installation has already ended
        if clicked_btn_txt != cancel_btn_txt:
            if not self.isBusy():
                QtGui.QMessageBox.information(
                    self,
                    "Sorry!",
                    "Cannot '%s' since the installation has ended!" % \
                        (clicked_btn_txt),
                    QtGui.QMessageBox.Ok)
                return
        
        # abort later
        if clicked_btn_txt == abort_later_btn_txt:
            if self.executor.isRunningLastTask():
                QtGui.QMessageBox.information(
                    self,
                    "Sorry!",
                    "Cannot '%s' since the last installation process is "\
                        "running!" % abort_later_btn_txt, 
                    QtGui.QMessageBox.Ok)
            else:
                self.executor.abort()

        # abort now: kill self & children (running installers) too
        elif clicked_btn_txt == abort_now_btn_txt:
            LOG("Committing suicide...")
            os.killpg(os.getpid(), signal.SIGTERM)

    def showEvent(self, event):
        '''Overriden method of QWidget. Scrolls the slider of the vertical
        scroll bar of the text edit to the end.  This is done to enable
        automatic scrolling of the scrollbar upon addition of the new text.
        Done only once, when the page is shown for the first time.'''
        QtGui.QWizardPage.showEvent(self, event)

        if not self.scrolled_to_vend:
            self.logsTextEditScrollToVend()
            self.scrolled_to_vend = True

    def logsButtonToggled(self, checked):
        '''Slot for the toggled signal of the logs button. Hides/shows logs
        widgets.'''
        self.logs_text_edit.setVisible(checked)
        self.logs_url_label.setVisible(checked)

        if checked:
            # when text edit is visible spacer is removed, otherwise spacer
            # would visibly consume space if page is resized
            self.layout().removeItem(self.spacer)

        else:
            # when text edit is hidden the spacer is used to take its space to
            # prevent other widget from moving in the layout
            self.layout().insertItem(2, self.spacer)

    def logsTextEditScrollToVend(self):
        '''Scrolls vertical scroll bar of the text edit widget to the end'''
        vsb = self.logs_text_edit.verticalScrollBar()
        vsb.setValue(vsb.maximum())

    def logsTextEditRemoveLastLine(self):
        '''Removes last line from the text edit'''
        cursor = self.logs_text_edit.textCursor()
        cursor.movePosition(QtGui.QTextCursor.End)
        cursor.select(QtGui.QTextCursor.BlockUnderCursor)
        cursor.deletePreviousChar()

    def splitStringWithMultiCR(self, txt):
        '''Splits a string containing multiple lines of text into list a of
        single-line strings. Character \r is assumed to mark the beginning of a
        line, whereas character \n is assumed to mark the end of a line. This
        routine is used to emulate terminal carriage return (\r) handling.

        returns a list of strings
        txt = a string containing multiple lines of text (including \n, \r etc)
        '''
        txt_list = []

        i_begin = 0 # index, where the next line should begin from

        for i in xrange(0, len(txt)):

            # \r is assumed to mark the beginning of a new line
            if txt[i] == '\r':

                # there could be \r just after line ending with \n or the whole
                # text could begin with \r: we don't want empty string in those
                # cases
                if i - i_begin > 0:
                    txt_list.append(txt[i_begin:i])
                    i_begin = i

            # \n is assumed to mark the end of a current line, next char after
            # newline will be the start of the next line
            elif txt[i] == '\n':
                txt_list.append(txt[i_begin:i + 1])
                i_begin = i + 1

        # if text does not end with \n, get the last line
        if i_begin < len(txt):
            txt_list.append(txt[i_begin:len(txt)])

        return txt_list

    def logsTextEditAppend(self, txt):
        '''Appends text into the text edit widget. If there are carriage return
        characters in the text does some processing in order to emulate
        terminal behavior.

        txt = a string containing multiple lines of text (including \n, \r etc)
        '''
        vsb = self.logs_text_edit.verticalScrollBar()

        at_bottom = vsb.value() == vsb.maximum()

        # \r\n is just newline (apt uses it while selecting/unpacking)
        txt = txt.replace("\r\n", "\n")

        # number of carriage return characters in the text
        cr_count = txt.count('\r')

        # text without carriage return is just inserted
        if cr_count == 0:
            self.cursor.insertText(txt)

        # text has only one carriage return in the beginning, previous line
        # must be removed before the text is inserted
        elif cr_count == 1 and txt.startswith('\r'):
            self.logsTextEditRemoveLastLine() 
            self.cursor.insertText(txt)

        # text has multiple carriage return characters
        else:
            txt_list = self.splitStringWithMultiCR(txt)
            for line in txt_list:
                if line.startswith('\r'):
                    self.logsTextEditRemoveLastLine()
                self.cursor.insertText(line)

        # automatically scroll, if slider was at the end of scrollbar
        if at_bottom:
            self.logsTextEditScrollToVend()

    def tailProcessReadStdout(self):
        '''A slot that is called when the tail process has some text on its
        stdout. Appends that text to the text edit.'''
        txt = str(self.wizard().tail_process.readAllStandardOutput())
        self.logsTextEditAppend(txt)


class ConclusionPage(QtGui.QWizardPage):
    '''ConclusionPage page'''

    def __init__(self):
        '''Constructor'''

        QtGui.QWizardPage.__init__(self)

    def initializePage(self):
        '''Overrides the method of QWizardPage, to initialize the page with
        some default settings.'''
        self.setSubTitle(" ")
        if self.field(FNUninstall).toBool():
            self.setTitle('Uninstallation of %s Completed' % PRODUCT_NAME)
            layout = QtGui.QHBoxLayout()
            label = QtGui.QLabel("Scratchbox successfully uninstalled from system.\n\n"
                                 "If you did any changes to your scratchbox home directory, "
                                 "it is still available in system root, along with desktop "
                                 "shortcut (if you created one when installing). You can later "
                                 "re-install scratchbox over the old directory")
            label.setWordWrap(True)
            layout.addWidget(label)
        else:
            self.setTitle('Installation of %s Completed' % PRODUCT_NAME)

            layout = QtGui.QGridLayout()
            info_text_edit = QtGui.QTextEdit()
            info_text_edit.setReadOnly(True)

            instruction_title = QtGui.QLabel("Starting SDK UI")
            layout.addWidget(instruction_title, 0, 0)

            info_text_edit.setPlainText("1. Start the Xephyr xserver and Maemo desktop by clicking icon on the Desktop.\n\n"
                                        "2. Log out and log back in OR execute the following command to "
                                        "run scratchbox in the current terminal session.\n"
                                        "$ newgrp %s\n\n"
                                        "3. Login to scratchbox.\n"
                                        "$ %s/login\n\n"
                                        "Welcome to Scratchbox, the cross-compilation toolkit!\n"
                                        "Use 'sb-menu' to change your compilation target.\n"
                                        "See /scratchbox/doc/ for documentation.\n" % (SB_GROUP, SB_PATH))

            info_text_edit.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred);
            layout.addWidget(info_text_edit, 1, 0)
            link_title = QtGui.QLabel("Useful links")
            layout.addWidget(link_title, 0, 1)
            link_str = ('<a href="http://wiki.maemo.org/Documentation/Maemo_5_Developer_Guide/Graphical_UI_Tutorial/Getting_started">Get Started</a><br><br>'
                        '<a href="http://wiki.maemo.org/Documentation/Maemo_5_Developer_Guide">Maemo 5 Developer Guide</a><br><br>'
                        '<a href="http://www.forum.nokia.com/info/sw.nokia.com/id/eb8a68ba-6225-4d84-ba8f-a00e4a05ff6f/Hildon_2_2_UI_Style_Guide.html">Hildon 2.2 UI Style</a><br><br>'
                        '<a href="http://www.forum.nokia.com/info/sw.nokia.com/id/019c2b31-3777-49a0-9257-970d79580756/Hildon_2_2_Widget_UI_Specification.html">Hildon 2.2 Widget UI Specification</a><br><br>'
                        '<a href="http://wiki.maemo.org/Documentation/Maemo_5_Developer_Guide/Porting_Software/Redesigning_From_Maemo_4_to_Maemo_5">Redesigning from Maemo 4 to Maemo 5</a><br><br>'
                        '<a href="https://garage.maemo.org/svn/maemoexamples/trunk/">Example code</a><br><br>'
                        '<a href="http://maemo.org/development/sdks/maemo_5_api_documentation/">API References</a><br><br><br><br>')
            self.link_label = QtGui.QLabel()
            self.link_label.setText(link_str)
            self.link_label.setAlignment(QtCore.Qt.AlignTop)
            self.connect(self.link_label, QtCore.SIGNAL("linkActivated(QString)"), self.LoadUrl)
            layout.addWidget(self.link_label, 1, 1)

        self.setLayout(layout)


    def LoadUrl(self, url):
        '''Loads specified URL in browser. Tries to figure out prefered one.'''
        #If we just let Qt open browser, it will run as root, and we don't want that.
        browsers = ['x-www-browser', 'firefox', 'iceweasel', 'opera', 'konqueror']
        exe = Which('xdg-open')
        if exe:
            command = exe + ' ' + str(url)
            user = str(self.field(FNSelectedUsername).toString())

            if user:
                exec_cmd(command, username = user, donotwait = True)
            else:
                exec_cmd(command, donotwait = True)
            return
        #xdg isn't installed as default on some distros, so we'll check if user has one
        #of listed browsers, and use that.
        for browser in browsers:
            exe = Which(browser)
            if exe:
                command = exe + ' ' + str(url)
                user = str(self.field(FNSelectedUsername).toString())
                if user:
                    exec_cmd(command, username = user, donotwait = True)
                else:
                    exec_cmd(command, donotwait = True)
                return


PageIdProxy          = 0
PageIdIntro          = 1
PageIdLevel          = 2
PageIdLicense        = 3
PageIdUsers          = 4
PageIdInstallOpts    = 5
PageIdTargets        = 6
PageIdPkg            = 7
PageIdNokiaBins      = 8
PageIdSummary        = 9
PageIdProgress       = 10
PageIdConclusion     = 11

class InstallWizard(QtGui.QWizard):
    '''Installation wizard'''

    def __init__(self):
        '''Constructor'''

        # create own group so can kill (Abort Now) self & children during
        # installation: this is redundant in shells but needed if script is
        # launched from desktop e.g. by using kdesudo
        global LOG
        os.setpgrp()

        QtGui.QWizard.__init__(self)

        self.setWindowTitle(MY_NAME)

        self.__host_info = HostInfo()
        set_proxy(None)

        # process that tails the log file
        self.tail_process = QtCore.QProcess(self)
        self.tail_process.start("tail -f " + LOG.fn_log)

        self.setOption(QtGui.QWizard.DisabledBackButtonOnLastPage)
        self.imageHandler = ImageHandler(IMAGES)

        #Check system proxy settings
        for param in os.environ.keys():
            for key in ['http_proxy', 'HTTP_PROXY']:
                if param == key:
                    set_proxy(os.environ[param])
                    break

        if not self.imageHandler.downloadImage(random.randint(0, len(IMAGES)-1)):
            self.setPage(PageIdProxy, ProxyPage())

        self.setPage(PageIdIntro, IntroPage())
        self.setPage(PageIdLevel, LevelPage(self.__host_info.has_scratchbox, self.__host_info.has_apt_get))
        self.setPage(PageIdLicense, LicensePage(self.__host_info.has_64bit))
        self.setPage(PageIdUsers, UsersPage(self.__host_info.has_64bit))
        self.setPage(PageIdInstallOpts, InstallOptsPage(self.__host_info.has_scratchbox,
                                                        self.__host_info.scratchbox_op_name,
                                                        self.__host_info.has_xephyr,
                                                        self.__host_info.has_apt_get or
                                                        self.__host_info.has_yum or
                                                        self.__host_info.has_zypper))

        self.setPage(PageIdTargets, TargetsPage())
        self.setPage(PageIdPkg, PkgPage())
        if HAVE_WEBKIT:
            self.setPage(PageIdNokiaBins, NokiaBinsPage())
        else:
            self.setPage(PageIdNokiaBins, NokiaBinsPageNoWebkit())



        self.setPage(PageIdSummary, SummaryPage())
        self.setPage(PageIdProgress, ProgressPage(self.tail_process))
        self.setPage(PageIdConclusion, ConclusionPage())

    hostInfo = property(lambda self: self.__host_info)

    def __del__(self):
        '''destructor'''
        self.tail_process.kill()

    def reject(self):
        '''Overridden method of QDialog to disable wizard closing when
        installation/removal is in progress. Handles closing wizard by:
        - pressing Esc button
        - clicking Cancel button
        - clicking X button on the title bar of the window
        - any shortcut that can be used to close a window
        - probably any other means used to close a window'''

        if self.currentId() == PageIdProgress and self.currentPage().isBusy():

            self.currentPage().onCancel()
            return

        # default behavior in other cases
        QtGui.QWizard.reject(self)

    def disabled_nextId(self):
        '''Returns ID of page to show when the user clicks the Next button.

        After the Users page if SDK not to be installed, returns the summary
        page. Targets page is not shown if targets don't exist for the selected
        user.
        '''
        if QtGui.QWizard.nextId(self) == PageIdTargets:
            if self.field(FNEasyInstall).toBool():
                return QtGui.QWizard.nextId(self)
            else:
                install_sdk = self.field(FNInstallSDK).toBool()
                if not install_sdk:
                    return PageIdSummary

                else: # sdk will be installed
                    target_x86_exist = self.field(FNTargetX86Exist).toBool()
                    target_armel_exist = self.field(FNTargetArmelExist).toBool()
                    if not target_x86_exist and not target_armel_exist:
                        return PageIdTargets + 1 # next page
        if QtGui.QWizard.nextId(self) == PageIdNokiaBins:
            if self.field(FNSDKInstMOptArg).toString() == INSTALL_OPTIONS[0][1]:
                return PageIdNokiaBins + 1

        return QtGui.QWizard.nextId(self)




def run_checks():
    '''Performs some system checks, raises exception if some check fails'''

    # this script must run with root privileges: installation of SDK only could
    # be done without root priveleges, but the SDK installer requires VDSO to
    # be set to scratchbox compatible value, which can only be done as root

    if os.geteuid() != 0:
        raise Exception("Root access is needed! Please run this application "
                        "with root privileges!")

    # only 64 and 32 bit X86 systems are supported
    sup_machines = ["i386", "i686", "x86_64"]
    machine = os.uname()[4]

    if machine not in sup_machines:
        raise Exception("Operating system's machine or word size '%s' is not "
                        "supported. Only 32- and 64bit X86 systems are supported %s." %
                        (machine, sup_machines))


def main():
    '''Main.'''
    app = QtGui.QApplication([])

    parse_options()

    try:
        run_checks()
    except Exception, e:
        QtGui.QMessageBox.critical(None, MY_NAME, str(e), QtGui.QMessageBox.Ok)
        sys.exit(1)
    random.seed()
    global LOG
    LOG = Logger() # must have root acces to re-write previous log file
    socket.setdefaulttimeout(30)
    wizard = InstallWizard()
    wizard.setWizardStyle(QtGui.QWizard.ModernStyle)
    wizard.resize(WINDOW_WIDTH, WINDOW_HEIGHT)
    wizard.show()
    #Older PyQt versions don't seem to set window size before it's opened
    wizard.resize(WINDOW_WIDTH, WINDOW_HEIGHT)

    return app.exec_()


if __name__ == "__main__":
    main()
