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

# ubuntuone-client-applet - Tray icon applet for managing Ubuntu One
#
# Author: Rodney Dawes <rodney.dawes@canonical.com>
#
# Copyright 2009 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, 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/>.

from __future__ import division

import pygtk
pygtk.require('2.0')
import gobject
import gtk
import os
import sys
import time
import gettext
import gnomekeyring
import simplejson
import subprocess
from threading import Thread
from oauth import oauth
from ubuntuone import clientdefs
from ubuntuone.syncdaemon.tools import SyncDaemonTool
from ubuntuone.logger import (basic_formatter, LOGFOLDER,
                              CustomRotatingFileHandler)

import logging
import httplib, urlparse, socket

import dbus.service
from dbus.exceptions import DBusException
from dbus.mainloop.glib import DBusGMainLoop

logger = logging.getLogger("ubuntuone-preferences")
logger.setLevel(logging.DEBUG)
handler = CustomRotatingFileHandler(filename=os.path.join(LOGFOLDER,
                                                          'u1-prefs.log'))
handler.setFormatter(basic_formatter)
handler.setLevel(logging.DEBUG)
logger.addHandler(handler)

_ = gettext.gettext

dcouch = None
# Try importing the Ubuntu One desktopcouch enablement api
try:
    from desktopcouch.replication_services import ubuntuone as dcouch
except ImportError:
    logger.error(_("DesktopCouch replication API not found"))

DBusGMainLoop(set_as_default=True)

DBUS_IFACE_NAME = "com.ubuntuone.SyncDaemon"
DBUS_IFACE_CONFIG_NAME = DBUS_IFACE_NAME + ".Config"
DBUS_IFACE_STATUS_NAME = DBUS_IFACE_NAME + ".Status"

DBUS_IFACE_AUTH_NAME = "com.ubuntuone.Authentication"
DBUS_IFACE_AUTH_PATH = "/"

# Our own DBus interface
PREFS_BUS_NAME = "com.ubuntuone.Preferences"

# This is where thy music lies
U1MSPATH = os.path.expanduser("~/.ubuntuone/Purchased from Ubuntu One")

# Why thank you GTK+ for enforcing style-set and breaking API
RCSTYLE = """
style 'dialogs' {
  GtkDialog::action-area-border = 12
  GtkDialog::button-spacing = 6
  GtkDialog::content-area-border = 0
}
widget_class '*Dialog*' style 'dialogs'
"""

# Some defines for the bindwood installation magic
BW_PKG_NAME = "xul-ext-bindwood"
BW_INST_ARGS = ['apturl', 'apt:%s?section=universe' % BW_PKG_NAME]
BW_CHCK_ARGS = ['dpkg', '-l', BW_PKG_NAME]

# This is a global so we can avoid creating multiple instances in some cases
prefs_dialog = None

def dbus_async(*args, **kwargs):
    """Simple handler to make dbus do stuff async."""
    pass


def do_login_request(bus, error_handler):
    """Make a login request to the login handling daemon."""
    try:
        client = bus.get_object(DBUS_IFACE_AUTH_NAME,
                                DBUS_IFACE_AUTH_PATH,
                                follow_name_owner_changes=True)
        iface = dbus.Interface(client, DBUS_IFACE_AUTH_NAME)
        iface.login('https://ubuntuone.com', 'ubuntuone',
                    reply_handler=dbus_async,
                    error_handler=error_handler)
    except DBusException, e:
        error_handler(e)

def get_access_token(keyring):
    """Get the access token from the keyring."""
    items = []
    try:
        items = keyring.find_items_sync(
            keyring.ITEM_GENERIC_SECRET,
            {'ubuntuone-realm': "https://ubuntuone.com",
             'oauth-consumer-key': 'ubuntuone'})
        secret = items[0].secret
        return oauth.OAuthToken.from_string(secret)
    except (gnomekeyring.NoMatchError, gnomekeyring.DeniedError):
        return None

def do_rest_request(url, method, token, callback):
    """
    Helper that handles the REST response.
    """
    result = None
    consumer = oauth.OAuthConsumer('ubuntuone', 'hammertime')

    oauth_request = oauth.OAuthRequest.from_consumer_and_token(
        http_url=url,
        http_method=method,
        oauth_consumer=consumer,
        token=token,
        parameters='')
    oauth_request.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
                               consumer, token)

    scheme, netloc, path, query, fragment = urlparse.urlsplit(url)

    conn = httplib.HTTPSConnection(netloc)
    try:
        conn.request(method, path, headers=oauth_request.to_header())
        response = conn.getresponse() # shouldn't block
        if response.status == 200:
            data = response.read() # neither should this
            result = simplejson.loads(data)
        else:
            result = {'status' : response.status,
                      'reason' : response.reason}
    except socket.error, e:
        result = {'error' : e}

    gtk.gdk.threads_enter()
    callback(result)
    gtk.gdk.threads_leave()

def make_rest_request(url=None, method='GET',
                      callback=None, keyring=None):
    """
    Helper that makes an oauth-wrapped REST request.
    """
    token = get_access_token(keyring)
    Thread(target=do_rest_request, args=(url, method, token, callback)).start()


class DevicesWidget(gtk.Table):
    """
    the Devices tab.
    """
    def __init__(self,
                 bus,
                 keyring=gnomekeyring,
                 realm='https://ubuntuone.com',
                 consumer_key='ubuntuone',
                 url='https://one.ubuntu.com/api/1.0/devices/'):
        super(DevicesWidget, self).__init__(rows=2, columns=3)
        self.bus = bus
        self.keyring = keyring
        self.sdtool = SyncDaemonTool(bus)
        self.set_border_width(6)
        self.set_row_spacings(6)
        self.set_col_spacings(6)
        self.devices = None
        self.realm = realm
        self.consumer_key = consumer_key
        self.base_url = url
        self.conn = None
        self.consumer = None
        self.table_widgets = []

        self.connected = None  # i.e. unknown
        self.conn_btn = None
        self.up_spin = None
        self.dn_spin = None
        self.bw_chk = None
        self.bw_limited = False
        self.up_limit = 2097152
        self.dn_limit = 2097152

        self._update_id = 0

        self.status_label = gtk.Label("")
        self.attach(self.status_label, 0, 3, 2, 3)

        self.description = gtk.Label(_("The devices connected to with your"
                                       " personal cloud network"
                                       " are listed below"))
        self.description.set_alignment(0., .5)
        self.description.set_line_wrap(True)
        self.attach(self.description, 0, 3, 0, 1, xpadding=12, ypadding=12)

    def update_bw_settings(self):
        """
        Push the bandwidth settings at syncdaemon.
        """
        if self.sdtool.is_files_sync_enabled():
            self.sdtool.set_throttling_limits(self.dn_limit or -1,
                                              self.up_limit or -1)
            self.sdtool.enable_throttling(self.bw_limited)

    def handle_bw_controls_changed(self, *a):
        """
        Sync the bandwidth throttling model with the view.

        Start a timer to sync with syncdaemon too.
        """
        if not self.sdtool.is_files_sync_enabled():
            return
        # Remove the timeout ...
        if self._update_id != 0:
            gobject.source_remove(self._update_id)

        # sync the model ...
        self.bw_limited = self.bw_chk.get_active()
        self.up_limit = self.up_spin.get_value_as_int() * 1024
        self.dn_limit = self.dn_spin.get_value_as_int() * 1024

        # ... and add the timeout back
        self._update_id = gobject.timeout_add_seconds(
              1, self.update_bw_settings)

    def handle_bw_checkbox_toggled(self, checkbox, *widgets):
        """
        Callback for the bandwidth togglebutton.
        """
        active = checkbox.get_active()
        for widget in widgets:
            widget.set_sensitive(active)
        self.handle_bw_controls_changed()

    def handle_limits(self, limits):
        """
        Callback for when syncdaemon tells us its throttling limits.
        """
        self.up_limit = int(limits['upload'])
        self.dn_limit = int(limits['download'])
        if self.up_spin is not None and self.dn_spin is not None:
            self.up_spin.set_value(self.up_limit / 1024)
            self.dn_spin.set_value(self.dn_limit / 1024)

    def handle_throttling_enabled(self, enabled):
        """
        Callback for when syncdaemon tells us whether throttling is enabled.
        """
        self.bw_limited = enabled
        if self.bw_chk is not None:
            self.bw_chk.set_active(enabled)

    def handle_state_change(self, new_state):
        """
        Callback for when syncdaemon's state changes.
        """
        if new_state['is_error']:
            # this syncdaemon isn't going to connect no more
            self.connected = None
        else:
            self.connected = new_state['is_connected']
        if self.conn_btn is not None:
            if self.connected:
                self.conn_btn.set_label(_("Disconnect"))
            else:
                self.conn_btn.set_label(_("Connect"))
            if self.connected is None:
                self.conn_btn.set_sensitive(False)
            else:
                self.conn_btn.set_sensitive(True)

    def error(self, msg):
        """
        Clear the table and show the error message in its place.

        This might be better as an error dialog.
        """
        logger.error(msg)
        dialog = gtk.MessageDialog(self.get_toplevel(),
                                   gtk.DIALOG_DESTROY_WITH_PARENT | 
                                   gtk.DIALOG_MODAL,
                                   gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
                                   'Error')
        dialog.format_secondary_text(str(msg))
        try:
            dialog.run()
        finally:
            dialog.destroy()
        while gtk.events_pending():
            gtk.main_iteration()

    def get_devices(self):
        """
        Ask the server for a list of devices

        Hook up parse_devices to run on the result (when it gets here).
        """
        try:
            get_access_token(self.keyring)
        except gnomekeyring.NoMatchError:
            self.error("No token in the keyring")
            self.devices = None
        else:
            make_rest_request(url=self.base_url, keyring=self.keyring,
                              callback=self.parse_devices)

    def parse_devices(self, result):
        """
        Parse the list of devices, and hook up list_devices if it worked.
        """
        error = None
        if result and isinstance(result, list):
            self.devices = result
        elif isinstance(result, dict):
            error = result.get('error', None)
            if not error and result.get('status', None):
                error = result.get('reason', None)
            else:
                error = "Invalid response getting devices list."
        else:
            error = "Got empty result for devices list."

        if error:
            self.devices = []
            logger.error(error)

        gobject.idle_add(self.list_devices)

    def clear_devices_view(self):
        """
        Clear out almost all the widgets.

        All except from the table, the description and the
        status_label get destroyed.
        """
        for i in self.get_children():
            if i not in (self.description, self.status_label):
                i.destroy()
        self.conn_btn = None
        self.up_spin = None
        self.dn_spin = None
        self.bw_chk = None

    def list_devices(self):
        """
        Populate the table with the list of devices.

        If the list of devices is empty, make a fake one that refers
        to the local machine (to get the connect/restart buttons).
        """
        self.clear_devices_view()

        token = get_access_token(self.keyring)

        fsync_enabled = self.sdtool.is_files_sync_enabled()

        if not self.devices:
            if fsync_enabled:
                # a stopgap device so you can at least try to connect
                self.devices = [{'kind': 'Computer',
                                 'description': _("<LOCAL MACHINE>"),
                                 'token': token.key if token else '',
                                 'FAKE': 'YES'}]
        else:
            self.resize(len(self.devices)+1, 3)

        self.status_label.set_label("")

        i = 0
        for row in self.devices or ():
            i += 1
            img = gtk.Image()
            img.set_from_icon_name(row['kind'].lower(), gtk.ICON_SIZE_DND)
            desc = gtk.Label(row['description'])
            desc.set_alignment(0., .5)
            self.attach(img, 0, 1, i, i+1)
            self.attach(desc, 1, 2, i, i+1)
            if 'FAKE' not in row:
                # we don't include the "Remove" button for the fake entry :)
                butn = gtk.Button(_("Remove"))
                butn.connect('clicked', self.remove,
                             row['kind'], row.get('token'))
                self.attach(butn, 2, 3, i, i+1, xoptions=0, yoptions=0)
            if ((token and row.get('token') == token.key or 'FAKE' in row)
                and fsync_enabled):
                self.bw_chk = ck_btn = gtk.CheckButton(
                    _("_Limit Bandwidth Usage"))
                ck_btn.set_active(self.bw_limited)
                up_lbl = gtk.Label(_("Maximum _upload speed (KB/s):"))
                up_lbl.set_alignment(0., .5)
                up_lbl.set_use_underline(True)
                adj = gtk.Adjustment(value=self.up_limit/1024.,
                                     lower=0.0, upper=4096.0,
                                     step_incr=1.0, page_incr=16.0)
                self.up_spin = up_btn = gtk.SpinButton(adj)
                up_btn.connect("value-changed", self.handle_bw_controls_changed)
                up_lbl.set_mnemonic_widget(up_btn)
                dn_lbl = gtk.Label(_("Maximum _download speed (KB/s):"))
                dn_lbl.set_alignment(0., .5)
                dn_lbl.set_use_underline(True)
                adj = gtk.Adjustment(value=self.dn_limit/1024.,
                                     lower=0.0, upper=4096.0,
                                     step_incr=1.0, page_incr=16.0)
                self.dn_spin = dn_btn = gtk.SpinButton(adj)
                dn_btn.connect("value-changed", self.handle_bw_controls_changed)
                dn_lbl.set_mnemonic_widget(dn_btn)
                ck_btn.connect('toggled', self.handle_bw_checkbox_toggled,
                               up_lbl, up_btn, dn_lbl, dn_btn)
                if fsync_enabled:
                    self.handle_bw_checkbox_toggled(ck_btn, up_lbl, up_btn,
                                                    dn_lbl, dn_btn)

                self.conn_btn = gtk.Button(_("Connect"))
                if self.connected is None:
                    self.conn_btn.set_sensitive(False)
                elif self.connected:
                    self.conn_btn.set_label(_("Disconnect"))
                self.conn_btn.connect('clicked', self.handle_connect_button)
                restart_btn = gtk.Button(_("Restart"))
                restart_btn.connect('clicked', self.handle_restart_button)
                btn_box = gtk.HButtonBox()
                btn_box.add(self.conn_btn)
                btn_box.add(restart_btn)

                i += 1
                self.attach(ck_btn, 1, 3, i, i+1)
                i += 1
                self.attach(up_lbl, 1, 2, i, i+1)
                self.attach(up_btn, 2, 3, i, i+1)
                i += 1
                self.attach(dn_lbl, 1, 2, i, i+1)
                self.attach(dn_btn, 2, 3, i, i+1)
                i += 1
                self.attach(btn_box, 1, 3, i, i+1)
            i += 2
        self.show_all()

    def handle_connect_button(self, *a):
        """
        Callback for the Connect/Disconnect button.
        """
        self.conn_btn.set_sensitive(False)
        if self.connected:
            self.sdtool.disconnect()
        else:
            self.sdtool.connect()

    def handle_restart_button(self, *a):
        """
        Callback for the Restart button.
        """
        self.sdtool.quit().addCallbacks(lambda _: self.sdtool.start())

    def remove(self, button, kind, token):
        """
        Callback for the Remove button.

        Starts an async request to remove a device.
        """
        make_rest_request(url=('%sremove/%s/%s' % (self.base_url,
                                                   kind.lower(), token)),
                          keyring=self.keyring,
                          callback=self.parse_devices)
        local = get_access_token(self.keyring)
        def local_removal_cb(*args, **kwargs):
            """Try to get a new token if we remove the local one."""
            do_login_request(self.bus, self.error)

        if token == local.key:
            try:
                client = self.bus.get_object(DBUS_IFACE_AUTH_NAME,
                                             DBUS_IFACE_AUTH_PATH,
                                             follow_name_owner_changes=True)
                iface = dbus.Interface(client, DBUS_IFACE_AUTH_NAME)
                iface.clear_token('https://ubuntuone.com', 'ubuntuone',
                                  reply_handler=local_removal_cb,
                                  error_handler=self.error)
            except DBusException, e:
                self.error(e)


class UbuntuOneDialog(gtk.Dialog):
    """Preferences dialog."""

    def __init__(self, config=None, keyring=gnomekeyring, *args, **kw):
        """Initializes our config dialog."""
        super(UbuntuOneDialog, self).__init__(*args, **kw)
        self.set_title(_("Ubuntu One Preferences"))
        self.set_has_separator(False)
        self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
        self.set_default_response(gtk.RESPONSE_CLOSE)
        self.set_icon_name("ubuntuone")
        self.set_default_size(400, 300)

        self.connect("close", self.__handle_response, gtk.RESPONSE_CLOSE)
        self.connect("response", self.__handle_response)

        # desktopcouch ReplicationExclusion, or None
        self.dcouch = None

        # folder id for dealing with the Music folder
        self.ums_id = None

        self.__bus = dbus.SessionBus()
        self.keyring = keyring

        # Timeout ID to avoid spamming DBus from spinbutton changes
        self.__update_id = 0

        # Build the dialog
        self.__construct()

        # SD Tool object
        self.sdtool = SyncDaemonTool(self.__bus)
        if self.sdtool.is_files_sync_enabled():
            self._hookup_dbus()
        else:
            self.status_label.set_text(_("Disconnected"))

        logger.debug("starting")

    def _hookup_dbus(self):
        """
        Hook up dbus
        """
        self.sdtool.get_status().addCallbacks(self.__got_state,
                                              self.__sd_error)

        # hook dbus up
        self.__bus.add_signal_receiver(
            handler_function=self.__got_state,
            signal_name='StatusChanged',
            dbus_interface=DBUS_IFACE_STATUS_NAME)

        try:
            client = self.__bus.get_object(DBUS_IFACE_NAME, "/config",
                                           follow_name_owner_changes=True)
            iface = dbus.Interface(client, DBUS_IFACE_CONFIG_NAME)
            iface.get_throttling_limits(
                reply_handler=self.__got_limits,
                error_handler=self.__dbus_error)
            iface.bandwidth_throttling_enabled(
                reply_handler=self.__got_enabled,
                error_handler=self.__dbus_error)
        except DBusException, e:
            self.__dbus_error(e)


    def __dbus_error(self, error):
        """Error getting throttling config."""
        logger.error(error)

    def __sd_error(self, error):
        """Error talking to syncdaemon."""
        logger.error(error)

    def __got_state(self, state):
        """Got the state of syncdaemon."""
        if not state['is_connected']:
            self.status_label.set_text(_("Disconnected"))
        else:
            try:
                status = state['name']
                queues = state['queues']
            except KeyError:
                status = None
                queues = None
            if status == u'QUEUE_MANAGER' and queues == u'IDLE':
                self.status_label.set_text(_("Synchronization complete"))
            else:
                self.status_label.set_text(_(u"Synchronization in progress…"))

        # Update the stuff in the devices tab
        self.devices.handle_state_change(state)

    def __got_limits(self, limits):
        """Got the throttling limits."""
        logger.debug("got limits: %s" % (limits,))
        self.devices.handle_limits(limits)

    def __got_enabled(self, enabled):
        """Got the throttling enabled config."""
        self.devices.handle_throttling_enabled(enabled)

    def __handle_response(self, dialog, response):
        """Handle the dialog's response."""
        self.hide()
        while gtk.events_pending():
            gtk.main_iteration()
        self.devices.handle_bw_controls_changed()
        gobject.timeout_add_seconds(5, gtk.main_quit)

    def _format_for_gb_display(self, bytes):
        """Format bytes into reasonable gb display."""
        gb = bytes / 1024 / 1024 / 1024
        if gb < 1.0:
            mb = bytes / 1024 / 1024
            if mb < 1.0:
                return (bytes / 1024, 'KB')
            return (mb, 'MB')
        return (gb, 'GB')

    def update_quota_display(self, used, total):
        """Update the quota display."""
        percent = (float(used) / float(total)) * 100
        real_used, real_type = self._format_for_gb_display(used)
        self.usage_label.set_text(
            _("%(used)0.1f %(type)s Used (%(percent)0.1f%%)") % {
                'used' : real_used,
                'type' : real_type,
                'percent' : percent })
        self.usage_graph.set_fraction(percent / 100)

        # assumes all paid accounts will have more than 50GB
        is_paid_account = total >= 50<<30
        self.plan_label.set_label(_("Paid") if is_paid_account else _("Free"))
        if is_paid_account:
            self.upgrade_link.hide()
        else:
            self.upgrade_link.show()

        if percent >= 100.:
            self.overquota_img.set_from_icon_name('dialog-warning',
                                                  gtk.ICON_SIZE_DIALOG)
            self.overquota.show_all()
            label = _("You are using all of your Ubuntu One storage.")
            if is_paid_account:
                desc = _("Synchronization is now disabled. Remove files from"
                         " synchronization or upgrade your subscription. Use"
                         " the Support options if an upgrade is not available.")
            else:
                desc = _("Synchronization is now disabled. Remove files from"
                         " synchronization or upgrade your subscription.")
        elif percent >= 95.:
            self.overquota_img.set_from_icon_name('dialog-information',
                                                  gtk.ICON_SIZE_DIALOG)
            self.overquota.show_all()
            label = _("You are near your Ubuntu One storage limit.")
            if is_paid_account:
                desc = _("Automatic synchronization will stop when you reach"
                         " your storage limit.")
            else:
                desc = _("Automatic synchronization will stop when you reach"
                         " your storage limit. Please consider upgrading to"
                         " avoid a service interruption.")
        else:
            self.overquota_img.clear()
            label = desc = ""
            self.overquota.hide_all()
        self.overquota_label.set_markup("%s\n<small>%s</small>" % (label, desc))

    def got_quota_info(self, quota):
        """Handle the result from the quota REST call."""
        if quota:
            used = quota.get('used', 0)
            total = quota.get('total', 2)
            self.update_quota_display(used, total)

    def request_quota_info(self):
        """Request new quota info from server, and update display."""
        make_rest_request(url='https://one.ubuntu.com/api/quota/',
                          keyring=self.keyring,
                          callback=self.got_quota_info)

    def got_account_info(self, user):
        """Handle the result from the account REST call."""
        if user:
            self.name_label.set_text(user.get('nickname', _("Unknown")))
            self.mail_label.set_text(user.get('email', _("Unknown")))

    def request_account_info(self):
        """Request account info from server, and update display."""
        make_rest_request(url='https://one.ubuntu.com/api/account/',
                          keyring=self.keyring,
                          callback=self.got_account_info)

    def __bw_inst_cb(self, button):
        """Pops off the bindwood installer to a thread."""
        Thread(target=self.__install_bindwood).start()

    def __install_bindwood(self):
        """Runs the tool to intall bindwood and updates the UI."""
        installed = subprocess.call(BW_INST_ARGS)
        gtk.gdk.threads_enter()
        if installed == 0:
            self.bw_inst_box.hide()
        else:
            self.bw_inst_box.show()
            logger.error(_("There was an error installing bindwood."))
        gtk.gdk.threads_leave()

    def __check_for_bindwood(self):
        """
        Method run in a thread to check that bindwood exists, and
        update the interface appropriately.
        """
        exists = True
        p = subprocess.Popen(BW_CHCK_ARGS, bufsize=4096,
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                             stdin=subprocess.PIPE, env=os.environ)
        result = p.wait()
        if result == 0:
            for line in p.stdout.readlines():
                if line.startswith('un'):
                    exists = False
        else:
            exists = False

        gtk.gdk.threads_enter()
        if exists:
            self.bw_inst_box.hide()
        else:
            self.bw_inst_btn.connect("clicked",
                                     self.__bw_inst_cb)
            self.bw_inst_box.show()
        gtk.gdk.threads_leave()

    def connect_desktopcouch_exclusion(self):
        """Hook up to desktopcouch enablement API, or disable the UI."""
        # Hook up to desktopcouch, or die trying
        if dcouch:
            Thread(target=self.__check_for_bindwood).start()

            self.dcouch = dcouch.ReplicationExclusion()
            exclusions = self.dcouch.all_exclusions()
            for check in [self.bookmarks_check,
                          self.abook_check,
                          self.gwib_check]:
                if check.get_data('dbname') in exclusions:
                    check.set_active(False)
                else:
                    check.set_active(True)
        else:
            self.bookmarks_check.set_sensitive(False)
            self.abook_check.set_sensitive(False)
            self.gwib_check.set_sensitive(False)

    def toggle_db_sync(self, dbname, disable=False):
        """
        Toggle whether a db in desktopcouch is synchronized to the
        Ubuntu One couchdb server.
        """
        if self.dcouch:
            if disable:
                self.dcouch.exclude(dbname)
            else:
                self.dcouch.replicate(dbname)
        else:
            logger.error(_("Database enablement is unavailable."))

    def __db_check_toggled(self, checkbutton):
        """Handle toggling a desktopcouch service toggling."""
        dbname = checkbutton.get_data('dbname')
        self.toggle_db_sync(dbname, not checkbutton.get_active())

    def files_check_toggled(self, checkbutton):
        """Handle toggling the files service."""
        enabled = checkbutton.get_active()
        self.sdtool.enable_files_sync(enabled).addCallbacks(
            lambda _: dbus_async,
            self.__sd_error)
        def sd_enabled(_):
            self._hookup_dbus()
            self.sdtool.connect()
            self.devices.list_devices()
        def sd_error(error):
            self.files_check.set_active(False)
            self.files_check.set_sensitive(False)
            self.music_check.set_sensitive(False)
            self.__sd_error(error)

        if enabled:
            self.sdtool.start().addCallbacks(sd_enabled,
                                             sd_error)
            if self.ums_id:
                self.music_check.set_sensitive(True)
                if self.music_check.get_active():
                    self.music_check_toggled(self.music_check)
        else:
            self.sdtool.quit().addCallback(
                lambda _: self.devices.list_devices())
            self.music_check.set_sensitive(False)

    def music_check_toggled(self, checkbutton):
        """Handle toggling the music download service."""
        if not self.files_check.get_active() or not self.ums_id:
            checkbutton.set_sensitive(False)
            checkbutton.set_active(False)
            return
        enabled = checkbutton.get_active()
        def got_error(error):
            checkbutton.set_sensitive(False)
            checkbutton.set_active(not enabled)
            self.__sd_error(error)

        if enabled:
            d = self.sdtool.subscribe_folder(self.ums_id)
        else:
            d = self.sdtool.unsubscribe_folder(self.ums_id)
        d.addErrback(got_error)

    def get_syncdaemon_sync_config(self):
        """Update the files/music sync config from syncdaemon."""
        def got_ms_err(error):
            self.music_check.set_sensitive(False)
            self.__sd_error(error)

        def got_info(info):
            if not info:
                self.music_check.set_active(False)
                self.music_check.set_sensitive(False)
            else:
                self.ums_id = info['volume_id']
                self.music_check.set_sensitive(True)
                if info['subscribed'] == 'True':
                    self.music_check.set_active(True)
                else:
                    self.music_check.set_active(False)

        self.sdtool.get_folder_info(U1MSPATH).addCallbacks(got_info, got_ms_err)
        self.files_check.set_active(self.sdtool.is_files_sync_enabled())

    def connect_file_sync_callbacks(self):
        """Connect the file sync checkbox callbacks."""
        self.files_check.connect('toggled', self.files_check_toggled)
        self.music_check.connect('toggled', self.music_check_toggled)

    def __construct(self):
        """Construct the dialog's layout."""
        area = self.get_content_area()

        vbox = gtk.VBox(spacing=12)
        vbox.set_border_width(12)
        area.add(vbox)
        vbox.show()

        # Usage text/progress bar
        rbox = gtk.VBox(spacing=12)
        vbox.pack_start(rbox, False, False)
        rbox.show()

        hbox = gtk.HBox(spacing=6)
        rbox.pack_start(hbox, False, False)
        hbox.show()

        self.usage_graph = gtk.ProgressBar()
        hbox.pack_start(self.usage_graph, False, False)
        self.usage_graph.show()

        self.usage_label = gtk.Label(_("Unknown"))
        self.usage_label.set_alignment(0.0, 0.5)
        hbox.pack_start(self.usage_label, False, False)
        self.usage_label.show()

        self.status_label = gtk.Label(_("Unknown"))
        self.status_label.set_alignment(0.0, 0.5)
        rbox.pack_start(self.status_label, False, False)
        self.status_label.show()

        # Notebook
        self.notebook = gtk.Notebook()
        vbox.add(self.notebook)
        self.notebook.show()

        # Account tab
        account = gtk.VBox(spacing=12)
        account.set_border_width(6)
        self.notebook.append_page(account)
        self.notebook.set_tab_label_text(account, _("Account"))
        account.show()

        # overquota notice in account tab
        self.overquota = gtk.HBox(spacing=6)
        self.overquota.set_border_width(6)
        account.pack_start(self.overquota, False, False)

        self.overquota_img = gtk.Image()
        self.overquota.pack_start(self.overquota_img, False, False)
        self.overquota_img.show()

        self.overquota_label = label = gtk.Label("\n\n")
        label.set_line_wrap(True)
        label.set_alignment(0.0, 0.5)
        self.overquota.pack_start(label, False, False)
        self.overquota_label.show()


        # User info in account tab
        table = gtk.Table(rows=6, columns=2)
        table.set_row_spacings(6)
        table.set_col_spacings(6)
        account.pack_start(table, False, False)
        table.show()

        label = gtk.Label(_("_Name:"))
        label.set_use_underline(True)
        label.set_alignment(0.0, 0.5)
        table.attach(label, 0, 1, 0, 1)
        label.show()

        self.name_label = gtk.Label(_("Unknown"))
        self.name_label.set_use_underline(True)
        self.name_label.set_alignment(0.0, 0.5)
        table.attach(self.name_label, 1, 2, 0, 1)
        self.name_label.show()

        label = gtk.Label(_("_E-mail:"))
        label.set_use_underline(True)
        label.set_alignment(0.0, 0.5)
        table.attach(label, 0, 1, 1, 2)
        label.show()

        self.mail_label = gtk.Label(_("Unknown"))
        self.mail_label.set_use_underline(True)
        self.mail_label.set_alignment(0.0, 0.5)
        table.attach(self.mail_label, 1, 2, 1, 2)
        self.mail_label.show()

        label = gtk.Label(_("Current plan:"))
        label.set_alignment(0.0, 0.5)
        table.attach(label, 0, 1, 2, 3)
        label.show()

        self.plan_label = gtk.Label(_("Unknown"))
        self.plan_label.set_alignment(0.0, 0.5)
        table.attach(self.plan_label, 1, 2, 2, 3)
        self.plan_label.show()

        for n, (url, label) in enumerate([
                ("http://one.ubuntu.com/support", _("Support options")),
                ("http://one.ubuntu.com/account", _("Manage account")),
                ("http://one.ubuntu.com/upgrade",
                 _("Upgrade your subscription")),
            ]):
            link = gtk.LinkButton(url, label)
            link.set_relief(gtk.RELIEF_NONE)
            link.set_alignment(0.0, 0.5)
            table.attach(link, 0, 2, 5-n, 6-n)
            link.show()
        self.upgrade_link = link
        self.upgrade_link.hide()

        # Devices tab
        sw = gtk.ScrolledWindow()
        sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
        self.notebook.append_page(sw)
        self.notebook.set_tab_label_text(sw, _("Devices"))
        sw.show()
        self.devices = DevicesWidget(self.__bus, self.keyring)
        sw.add_with_viewport(self.devices)
        self.devices.list_devices()
        self.devices.show_all()

        # Services tab
        services = gtk.VBox(spacing=12)
        services.set_border_width(6)
        self.notebook.append_page(services)
        self.notebook.set_tab_label_text(services, _("Services"))
        services.show()

        label = gtk.Label()
        label.set_markup(
            _("Ubuntu One sync options\n"
              "<small>Choose services to synchronize"
              " with this computer</small>"))
        label.set_alignment(0., .5)
        label.set_padding(12, 6)
        label.set_line_wrap(True)
        services.pack_start(label, False, False)
        label.show()

        self.bookmarks_check = gtk.CheckButton(_("_Bookmarks"))
        self.bookmarks_check.set_data('dbname', 'bookmarks')
        self.bookmarks_check.connect('toggled', self.__db_check_toggled)
        services.pack_start(self.bookmarks_check, False, False)
        self.bookmarks_check.show()

        # This box is shown in the event bindwood is not installed
        self.bw_inst_box = gtk.HBox(spacing=12)
        services.pack_start(self.bw_inst_box, False, False)

        label = gtk.Label("")
        self.bw_inst_box.pack_start(label, False, False)
        label.show()

        label = gtk.Label("<i>%s</i>" % _("Firefox extension not installed."))
        label.set_use_markup(True)
        label.set_alignment(0.0, 0.5)
        self.bw_inst_box.pack_start(label, False, False)
        label.show()

        self.bw_inst_btn = gtk.Button(_("_Install"))
        self.bw_inst_box.pack_start(self.bw_inst_btn, False, False)
        self.bw_inst_btn.show()

        self.gwib_check = gtk.CheckButton(_("Broadcast Messages _Archive"))
        self.gwib_check.set_data('dbname', 'gwibber_messages')
        self.gwib_check.connect('toggled', self.__db_check_toggled)
        services.pack_start(self.gwib_check, False, False)
        self.gwib_check.show()

        self.abook_check = gtk.CheckButton(_("C_ontacts"))
        self.abook_check.set_data('dbname', 'contacts')
        self.abook_check.connect('toggled', self.__db_check_toggled)
        services.pack_start(self.abook_check, False, False)
        self.abook_check.show()

        fbox = gtk.VBox(spacing=6)
        services.pack_start(fbox, False, False)
        fbox.show()

        self.files_check = gtk.CheckButton(_("_File Synchronization"))
        self.files_check.set_active(False)
        fbox.pack_start(self.files_check, False, False)
        self.files_check.show()

        alignment = gtk.Alignment(0.0, 0.5)
        alignment.set_padding(0, 0, 12, 0)
        fbox.pack_start(alignment, False, False)
        alignment.show()

        self.music_check = gtk.CheckButton(_("_Music Download"))
        self.music_check.set_active(True)
        self.music_check.set_sensitive(False)
        alignment.add(self.music_check)
        self.music_check.show()


class UbuntuoneLoginHandler(dbus.service.Object):
    """Class to handle registration/login, in case we aren't already."""

    def __init__(self, dialog, *args, **kw):
        self.bus = dbus.SessionBus()

        # The actual UI
        self.dialog = dialog

        # DBus object magic
        self.path = '/'
        bus_name = dbus.service.BusName(PREFS_BUS_NAME, bus=self.bus)
        dbus.service.Object.__init__(self, bus_name=bus_name,
                                     object_path=self.path)


    @dbus.service.method(PREFS_BUS_NAME, in_signature='', out_signature='')
    def present(self):
        """Raise the dialog window."""
        self.dialog.connect_desktopcouch_exclusion()
        self.dialog.get_syncdaemon_sync_config()
        self.dialog.connect_file_sync_callbacks()
        self.dialog.request_quota_info()
        self.dialog.request_account_info()
        self.dialog.devices.get_devices()
        # watch it! jaunty has no 'get_visible' method
        if self.dialog.get_property('visible'):
            self.dialog.present_with_time(int(time.time()))

    def got_newcredentials(self, realm, consumer_key):
        """Show our dialog, since we can do stuff now."""
        self.present()

    def got_oautherror(self, message=None):
        """Got an error during oauth."""
        if message:
            logger.error(message)
        else:
            logger.error(_("OAuthError with no message."))

    def got_authdenied(self):
        """User denied access."""
        logger.error(_("Authorization was denied."))

    def got_dbus_error(self, error):
        """Got a DBusError."""
        logger.error(error)

    def register_signal_handlers(self):
        """Register the dbus signal handlers."""
        self.bus.add_signal_receiver(
            handler_function=self.got_newcredentials,
            signal_name='NewCredentials',
            dbus_interface=DBUS_IFACE_AUTH_NAME)
        self.bus.add_signal_receiver(
            handler_function=self.got_oautherror,
            signal_name='OAuthError',
            dbus_interface=DBUS_IFACE_AUTH_NAME)
        self.bus.add_signal_receiver(
            handler_function=self.got_authdenied,
            signal_name='AuthorizationDenied',
            dbus_interface=DBUS_IFACE_AUTH_NAME)


if __name__ == "__main__":
    gettext.bindtextdomain(clientdefs.GETTEXT_PACKAGE, clientdefs.LOCALEDIR)
    gettext.textdomain(clientdefs.GETTEXT_PACKAGE)

    gobject.threads_init()
    gtk.gdk.threads_init()

    gtk.rc_parse_string(RCSTYLE)
    gobject.set_application_name("ubuntuone-preferences")

    bus = dbus.SessionBus()
    result = bus.request_name(PREFS_BUS_NAME,
                              dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
    if result == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
        try:
            client = bus.get_object(PREFS_BUS_NAME, '/',
                                    follow_name_owner_changes=True)
            iface= dbus.Interface(client, PREFS_BUS_NAME)
            iface.present()
        except DBusException, e:
            logger.error(e)
        sys.exit(0)

    try:
        # The prefs dialog
        gtk.gdk.threads_enter()
        prefs_dialog = UbuntuOneDialog()
        prefs_dialog.show()

        login = UbuntuoneLoginHandler(dialog=prefs_dialog)
        login.register_signal_handlers()
        gobject.timeout_add_seconds(1, do_login_request,
                                    bus, login.got_dbus_error)
        gtk.main()
        gtk.gdk.threads_leave()
    except KeyboardInterrupt:
        pass
