#!/usr/bin/python

# 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 with_statement

import pygtk
pygtk.require('2.0')
import gobject
import gtk
import pango
import os
import subprocess
import sys
import gettext
from ubuntuone import clientdefs

import dbus.service

# pylint: disable-msg=F0401
import pynotify

from ConfigParser import ConfigParser
from dbus.exceptions import DBusException
from dbus.mainloop.glib import DBusGMainLoop
from ubuntuone.oauthdesktop.main import Login
from xdg.BaseDirectory import xdg_config_home
from threading import Lock, Thread
from urllib import quote

from ubuntuone.oauthdesktop.logger import setupLogging
logger = setupLogging("UbuntuOne.Client.Applet")

DBusGMainLoop(set_as_default=True)

_ = gettext.gettext
P_ = gettext.ngettext

APPLET_BUS_NAME = "com.ubuntuone.ClientApplet"
APPLET_CONFIG_NAME = APPLET_BUS_NAME + ".Config"

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

DBUS_IFACE_AUTH_NAME = "com.ubuntuone.Authentication"

OAUTH_REALM = "https://ubuntuone.com"
OAUTH_CONSUMER = "ubuntuone"
BOOKMARK_NAME = "Ubuntu One"

NOTIFY_ICON_SIZE = 48

# 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'
"""

CONF_FILE = os.path.join(xdg_config_home, "ubuntuone", "ubuntuone-client.conf")


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

class AppletMain(object):
      """Main applet process class."""

      def __init__(self, *args, **kw):
            """Initializes the child threads and dbus monitor."""
            from twisted.internet import gtk2reactor
            gtk2reactor.install()
            login = Login(dbus.service.BusName(DBUS_IFACE_AUTH_NAME,
                                               bus=dbus.SessionBus()))

            logger.info(_("Starting Ubuntu One client version %s") %
                        clientdefs.VERSION)

            # Whether or not we are authorized
            self.is_authorized = False

            # Load the config, with some defaults if it doesn't exist yet
            if not os.path.isdir(os.path.dirname(CONF_FILE)):
                  os.makedirs(os.path.dirname(CONF_FILE))

            self.config = ConfigParser()
            self.config.read(CONF_FILE)

            if not self.config.has_section("ubuntuone"):
                  self.config.add_section("ubuntuone")

            if not self.config.has_option("ubuntuone", "show_applet"):
                  self.config.set("ubuntuone", "show_applet", "1")

            if not self.config.has_option("ubuntuone", "connect"):
                  self.config.set("ubuntuone", "connect", "0")

            if not self.config.has_option("ubuntuone", "connected"):
                  self.config.set("ubuntuone", "connected", "False")

            if not self.config.has_option("ubuntuone", "bookmarked"):
                  self.config.set("ubuntuone", "bookmarked", "False")

            self.show_applet = self.config.getint("ubuntuone", "show_applet")
            self.connect = self.config.getint("ubuntuone", "connect")
            self.connected = self.config.getboolean("ubuntuone", "connected")

            if not os.path.exists(CONF_FILE):
                  with open(CONF_FILE, "w+b") as f:
                        self.config.write(f)

            # Handle some DBus signals
            self.__bus = dbus.SessionBus()
            self.__bus.add_signal_receiver(
                  handler_function=self.__new_credentials,
                  signal_name="NewCredentials",
                  dbus_interface=DBUS_IFACE_AUTH_NAME)
            self.__bus.add_signal_receiver(
                  handler_function=self.__auth_denied,
                  signal_name="AuthorizationDenied",
                  dbus_interface=DBUS_IFACE_AUTH_NAME)
            self.__bus.add_signal_receiver(
                  handler_function=self.__no_credentials,
                  signal_name="NoCredentials",
                  dbus_interface=DBUS_IFACE_AUTH_NAME)
            self.__bus.add_signal_receiver(
                  handler_function=self.__got_oauth_error,
                  signal_name="OAuthError",
                  dbus_interface=DBUS_IFACE_AUTH_NAME)

            self.__icon = AppletIcon(main=self, config=self.config)

      def __new_credentials(self, realm=None, consumer_key=None, sender=None):
            """Signal callback for when we get new credentials."""
            self.is_authorized = True

            self.__start_storage_daemon()
            self.add_to_autostart()
            
            self.set_up_desktopcouch_pairing(consumer_key)
            
            if self.connect == 2:
                  return

            if self.connect == 1 and not self.connected:
                  return

            try:
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
                                                 follow_name_owner_changes=True)
                  iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
                  iface.connect(reply_handler=dbus_async,
                                error_handler=self.sd_dbus_error)
            except DBusException, e:
                  self.sd_dbus_error(e)

      def __auth_denied(self):
            """Signal callback for when auth was denied by user."""
            self.is_authorized = False
            self.remove_from_autostart()

            def quit_error(e):
                  """Only log when quit fails."""
                  logger.error(_("Quit Error: %s") % e.get_dbus_message())

            try:
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
                      follow_name_owner_changes=True)
                  iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
                  iface.quit(reply_handler=dbus_async,
                             error_handler=quit_error)
            except DBusException, e:
                  quit_error(e)

            from twisted.internet import reactor
            reactor.stop()

      def __no_credentials(self):
            """Signal callback for when no credentials exist in the keyring."""
            self.is_authorized = False

      def __got_oauth_error(self, message=None):
            """Signal callback for when an OAuth error occured."""
            def dialog_response(dialog, response):
                  """Handle the dialog closing."""
                  dialog.destroy()

            if message:
                  logger.error(message)
                  dialog = gtk.Dialog(title=_("Ubuntu One: Error"),
                                      flags=gtk.DIALOG_NO_SEPARATOR,
                                      buttons=(gtk.STOCK_CLOSE,
                                               gtk.RESPONSE_CLOSE))
                  dialog.set_default_response(gtk.RESPONSE_CLOSE)
                  dialog.set_icon_name("ubuntuone-client")

                  area = dialog.get_content_area()

                  hbox = gtk.HBox(spacing=12)
                  hbox.set_border_width(12)
                  area.pack_start(hbox)
                  hbox.show()

                  image = gtk.Image()
                  image.set_from_icon_name("dialog-error", gtk.ICON_SIZE_DIALOG)
                  image.set_alignment(0.5, 0.0)
                  image.show()
                  hbox.pack_start(image, False, False)

                  vbox = gtk.VBox(spacing=12)
                  vbox.show()
                  hbox.pack_start(vbox)

                  label = gtk.Label("<b>%s</b>" % _("Authorization Error"))
                  label.set_use_markup(True)
                  label.set_alignment(0.0, 0.5)
                  label.show()
                  vbox.pack_start(label, False, False)

                  label = gtk.Label(message)
                  label.set_line_wrap(True)
                  label.set_max_width_chars(64)
                  label.set_ellipsize(pango.ELLIPSIZE_MIDDLE)
                  label.set_alignment(0.0, 0.0)
                  label.show()
                  vbox.pack_start(label, True, True)

                  dialog.connect('close', dialog_response, gtk.RESPONSE_CLOSE)
                  dialog.connect('response', dialog_response)

                  dialog.show()
            else:
                  logger.error(_("Got an OAuth error with no message."))

      def check_for_token(self, do_login=False):
            """Method to check for an existing token."""
            def local_dbus_error(e):
                  """Can't talk to ourself?"""
                  logger.error(_("Internal Error: %s") % e.get_dbus_message())

            try:
                  client = self.__bus.get_object(DBUS_IFACE_AUTH_NAME, "/",
                                                 follow_name_owner_changes=True)
                  iface = dbus.Interface(client, DBUS_IFACE_AUTH_NAME)
                  iface.maybe_login(OAUTH_REALM, OAUTH_CONSUMER,
                                    do_login,
                                    reply_handler=dbus_async,
                                    error_handler=local_dbus_error)
            except DBusException, e:
                  local_dbus_error(e)
                  return False

            return False

      def __start_storage_daemon_maybe(self):
            """Start the storage daemon."""
            try:
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
                                                 follow_name_owner_changes=True)
                  iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
                  iface.get_rootdir(reply_handler=dbus_async,
                                    error_handler=self.sd_dbus_error)
            except DBusException, e:
                  self.sd_dbus_error(e)
                  return False

            return False

      def __start_storage_daemon(self):
            """Need to call dbus from a idle callback"""
            gobject.idle_add(self.__start_storage_daemon_maybe)

      def add_to_autostart(self):
            """Add ourself to the autostart config."""
            autostart_entry = """[Desktop Entry]
Name=Ubuntu One
Exec=ubuntuone-client-applet
Icon=ubuntuone-client
Terminal=false
Type=Application
X-Ubuntu-Gettext-Domain=ubuntuone-client
X-KDE-autostart-after=panel
X-GNOME-Autostart-enabled=true
"""
            if not os.path.exists(os.path.join(xdg_config_home, "autostart")):
                  os.makedirs(os.path.join(xdg_config_home, "autostart"))

            file_path = os.path.join(xdg_config_home, "autostart",
                                     "ubuntuone-client-applet.desktop")
            if not os.path.exists(file_path):
                  with open(file_path, "w+") as f:
                        f.write(autostart_entry)

      def remove_from_autostart(self):
            """Remove ourself from the autostart config."""
            path = os.path.join(xdg_config_home, "autostart",
                                "ubuntuone-client-applet.desktop")
            try:
                  os.unlink(path)
            except OSError:
                  pass
      
      def set_up_desktopcouch_pairing(self, consumer_key):
          """Add a pairing record between desktopcouch and Ubuntu One"""
          try:
              from desktopcouch.pair.couchdb_pairing.couchdb_io import \
                   put_static_paired_service, PAIRED_SERVER_RECORD_TYPE
              from desktopcouch.records.server import CouchDatabase
          except ImportError:
              # desktopcouch is not installed
              logger.debug(_("Not adding desktopcouch pairing since"
                  " desktopcouch is not installed"))
              return
          # Check whether there is already a record of the Ubuntu One service
          db = CouchDatabase("management", create=True)
          if not db.view_exists("ubuntu_one_pair_record","ubuntu_one_pair_record"):
              map_js = """function(doc) {
                  if (doc.service_name == "ubuntuone") {
                      if (doc.application_annotations && 
                          doc.application_annotations["Ubuntu One"] &&
                          doc.application_annotations["Ubuntu One"]["private_application_annotations"] &&
                          doc.application_annotations["Ubuntu One"]["private_application_annotations"]["deleted"]) {
                          emit(doc._id, 1);
                      } else {
                          emit(doc._id, 0)
                      }
                  }
              }"""
              db.add_view("ubuntu_one_pair_record", map_js, None, 
                  "ubuntu_one_pair_record")
          results = db.execute_view("ubuntu_one_pair_record", 
              "ubuntu_one_pair_record")
          found = False
          deleted = False
          # Results should contain either one row or no rows
          # If there is one row, its value will be 0, meaning that there is
          #   already an Ubuntu One pairing record, or 1, meaning that there
          #   was an Ubuntu One pairing record but it has since been unpaired
          # Only create a new record if there is not one already. Specifically,
          #   do not add the record if there is a deleted one, as this means
          #   that the user explicitly unpaired it!
          for row in results:
              found = True
              if row.value == 1:
                  deleted = True
                  logger.debug(_("Not adding desktopcouch pairing since"
                  " the user has explicitly unpaired with Ubuntu One"))
              else:
                  logger.debug(_("Not adding desktopcouch pairing since"
                  " we are already paired"))
          if not found:
              put_static_paired_service(None, "ubuntuone")
              logger.debug(_("Pairing desktopcouch with Ubuntu One"))
      
      def main(self):
            """Starts the gtk main loop."""
            from twisted.internet import reactor
            if self.connect != 2 or (self.connect == 1 and self.connected):
                  gobject.idle_add(self.check_for_token, True)

            reactor.run()

      @property
      def authorized(self):
            """Are we authorized?"""
            return self.is_authorized

      def sd_dbus_error(self, error):
            """Got an error from DBus."""
            self.__icon.sd_dbus_error(error)



def do_config_open_real():
      """Opens the preferences dialog."""
      paths = os.environ["PATH"].split(":")
      dirpath = os.path.join(os.getcwd(), "bin")
      if os.path.isdir(dirpath):
            paths.insert(0, dirpath)
      os.environ["PATH"] = ":".join(paths)
      try:
            ret = subprocess.call(["ubuntuone-client-preferences"],
                                  env=os.environ)
      except OSError:
            ret = -1
      if ret != 0:
            logger.error(_("Failed to open Ubuntu One preferences"))

def do_config_open():
      """Do the preferences opening in a thread."""
      Thread(target=do_config_open_real, name="preferences").start()

def do_xdg_open_real(path_or_url):
      """Utility method to run xdg-open with path_or_url."""
      ret = subprocess.call(["xdg-open", path_or_url], env=os.environ)
      if ret != 0:
            logger.error(_("Failed to run 'xdg-open %s'") % path_or_url)

def do_xdg_open(path_or_url):
      """Do the xdg-open in a thread."""
      Thread(target=do_xdg_open_real, name="xdg", args=(path_or_url,)).start()


class AppletIcon(gtk.StatusIcon):
      """
      Custom StatusIcon derived from gtk.StatusIcon which supports
      animated icons and a few other nice things.
      """

      def __init__(self, main=None, config=None, *args, **kw):
            """Initializes our custom StatusIcon based widget."""
            super(AppletIcon, self).__init__(*args, **kw)
            # Hide until we get status, to avoid flickering
            self.set_visible(False)

            # The AppletMain object
            self.__main = main

            # A ConfigParser object that we can poke at
            self.__config = config
            self.__show_when = self.__config.getint("ubuntuone", "show_applet")

            self.__managed_dir = None

            self.__size = 24
            self.__theme = gtk.icon_theme_get_default()
            iconpath = os.path.abspath(os.path.join(
                        os.path.split(os.path.dirname(__file__))[0],
                        "data"))
            self.__theme.append_search_path(iconpath)

            self.__theme.append_search_path(os.path.sep + os.path.join(
                        "usr", "share", "ubuntuone-client", "icons"))
            self.__theme.append_search_path(os.path.sep + os.path.join(
                        "usr", "local", "share", "ubuntuone-client", "icons"))

            self.set_from_icon_name('ubuntuone-client-offline')
            self.set_tooltip(_("Disconnected"))
            self.connect("popup-menu", self.__popup_menu)
            self.connect("activate", self.__do_action)

            self.__size_changed(self, self.__size)

            self.__litems = {}
            self.__ritems = {}
            self.__status_menu, self.__config_menu = self.__build_menus()

            self.__connected = False
            self.__need_update = False
            self.__fatal_error = False

            pynotify.init("Ubuntu One")

            # Managing applet visibility
            self.__visible = True
            self.__visible_id = 0

            # Up/Dn status
            self.__lock = Lock()
            self.__updating = 0
            self.__total = 0
            self.__last_id = 0

            self.__bus = dbus.SessionBus()

            # Our own DBus service, for the config to deal with
            self.__service = AppletConfig(icon=self)

            # DBus signal handling
            self.__bus.add_signal_receiver(
                  handler_function=self.__auth_finished,
                  signal_name="NewCredentials",
                  dbus_interface=DBUS_IFACE_AUTH_NAME)

            self.__bus.add_signal_receiver(
                  handler_function=self.__status_changed,
                  signal_name="StatusChanged",
                  dbus_interface=DBUS_IFACE_STATUS_NAME)

            self.__bus.add_signal_receiver(
                  handler_function=self.__queue_changed,
                  signal_name="ContentQueueChanged",
                  dbus_interface=DBUS_IFACE_STATUS_NAME)
            self.__bus.add_signal_receiver(
                  handler_function=self.__transfer_started,
                  signal_name="UploadStarted",
                  dbus_interface=DBUS_IFACE_STATUS_NAME)
            self.__bus.add_signal_receiver(
                  handler_function=self.__transfer_started,
                  signal_name="DownloadStarted",
                  dbus_interface=DBUS_IFACE_STATUS_NAME)

            self.set_visible(True)

      def set_from_icon_name(self, icon):
            """Handle fallbacks for setting our icon."""
            pixbuf = self.__theme.load_icon(icon, self.__size,
                                            gtk.ICON_LOOKUP_GENERIC_FALLBACK)
            self.set_from_pixbuf(pixbuf)

      def set_visibility_config(self, visibility):
            """Update the visibility configuration."""
            self.__show_when = int(visibility)
            self.__config.set("ubuntuone", "show_applet", str(self.__show_when))
            self.update_visibility()

      def set_connection_config(self, connect):
            """Update the connection config."""
            self.__config.set("ubuntuone", "connect", str(connect))

      def __update_transfer_status(self, done=False):
            """Update the status display."""
            with self.__lock:
                  text = _("Updating %(transfers)d of %(total)d files...") % (
                        { 'transfers' : self.__updating,
                          'total' : self.__total})
            label = self.__litems["status"].get_child()
            if done:
                  self.set_tooltip(_("Files updated."))
                  self.set_from_icon_name("ubuntuone-client-idle")
                  label.set_text(_("Your files are up to date."))
            else:
                  label.set_markup("<i>%s</i>" % text)

      def __queue_changed(self, queue):
            """Handle ContentQueueChanged."""
            total = 0
            d = queue.get('Download', None)
            if d is not None:
                  total += int(d.get('count', 0))
            d = queue.get('Upload', None)
            if d is not None:
                  total += int(d.get('count', 0))
            first = False
            last = False
            self.__visible = True
            self.set_tooltip(_("Updating files..."))
            self.update_visibility()
            with self.__lock:
                  if self.__total == 0:
                        first = True
                        last = False
                  if self.__total != 0 and total == 0:
                        first = False
                        last = True
                  self.__total = total + self.__updating
            if first:
                  self.set_from_icon_name("ubuntuone-client-updating")
                  n = pynotify.Notification(
                        _("Updating files..."),
                        _("Ubuntu One is now updating your files."))
                  pixbuf = self.__theme.load_icon(
                        "ubuntuone-client-updating", NOTIFY_ICON_SIZE,
                        gtk.ICON_LOOKUP_GENERIC_FALLBACK)
                  n.set_icon_from_pixbuf(pixbuf)
                  n.show()
            if last:
                  if self.__last_id != 0:
                        gobject.source_remove(self.__last_id)
                        self.__last_id = 0
                  self.__last_id = gobject.timeout_add_seconds(
                        15, self.__updating_completed)

      def __updating_completed(self):
            """Timeout to avoid multiple started/finished notifications."""
            really_last = False
            n = None
            with self.__lock:
                  done = self.__total - self.__updating
                  if done == 0:
                        really_last = True
            if not really_last:
                  return False

            with self.__lock:
                  n = pynotify.Notification(
                        _("Updating Finished"),
                        P_("Ubuntu One finished updating %(total)d file.",
                           "Ubuntu One finished updating %(total)d files.",
                           self.__total) % { 'total' : self.__total })
                  self.__total = 0
                  self.__updating = 0
            pixbuf = self.__theme.load_icon(
                  "ubuntuone-client-updating", NOTIFY_ICON_SIZE,
                  gtk.ICON_LOOKUP_GENERIC_FALLBACK)
            n.set_icon_from_pixbuf(pixbuf)
            n.show()
            self.__update_transfer_status(True)
            return False

      def __transfer_started(self, path):
            """Handle the started signals."""
            with self.__lock:
                  self.__updating += 1
            self.__update_transfer_status()

      def update_visibility(self):
            """Update the icon's visibility."""
            if self.__visible_id != 0:
                  gobject.source_remove(self.__visible_id)
                  self.__visible_id = 0

            if (self.__visible and self.__show_when != 2) or self.__fatal_error:
                  self.set_visible(True)
                  return

            if self.__show_when == 2 and not self.__fatal_error:
                  self.set_visible(False)
                  return

            # If the icon is shown, set up a timeout to hide it
            if self.get_visible():
                  self.__visible_id = gobject.timeout_add_seconds(
                        30, self.__hide_icon)

      def __status_changed(self, status):
            """The sync daemon status changed."""
            if self.__managed_dir is None:
                gobject.idle_add(self.__get_root)

            if self.__show_when != 0:
                  self.__visible = False

            state = status['name']

            self.set_tooltip("Ubuntu One")

            if self.__fatal_error and state != "UNKNOWN_ERROR":
                  # Just blow your nose, and it's fixed, isn't it.
                  self.__fatal_error = False
                  self.__litems["connect"].set_sensitive(True)
                  self.__litems["disconnect"].set_sensitive(True)
                  
            if state == "OFFLINE" or state.startswith("INIT") or \
                      state.startswith("READY"):
                  self.set_from_icon_name("ubuntuone-client-offline")
                  self.set_tooltip(_("Disconnected"))
                  self.__connected = False
                  self.__visible = True

            elif state == "CAPABILITIES_MISMATCH":
                  self.__connected = False
                  self.__visible = True
                  # Pop up a notification
                  n = pynotify.Notification(
                        _("Capabilities Mismatch"),
                        _("There was a capabilities mismatch while attempting "
                          "to connect to the Ubuntu One server. You may "
                          "have installed a newer version of the client, for "
                          "which the server does not yet provide support. "
                          "A new version of the server should be accessible "
                          "soon. Please be patient while we update."))
                  pixbuf = self.__theme.load_icon(
                        "ubuntuone-client-error", NOTIFY_ICON_SIZE,
                        gtk.ICON_LOOKUP_GENERIC_FALLBACK)
                  n.set_icon_from_pixbuf(pixbuf)
                  n.set_urgency(pynotify.URGENCY_CRITICAL)
                  n.show()
                  # Set the tooltip and icon on the applet
                  self.set_tooltip(_("Capabilities mismatch with server."))
                  self.set_from_icon_name("ubuntuone-client-error")

            elif state == "IDLE" or state.startswith("READING") or \
                      state.startswith("SCANNING"):
                  self.__connected = True
                  if self.__show_when != 0:
                        self.__visible = False

            elif state == "AUTH_FAILED":
                  self.__stop_syncdaemon()
                  self.set_from_icon_name("ubuntuone-client-error")
                  self.set_tooltip(_("Authentication failed"))
                  self.__connected = False
                  self.__visible = True

                  def reauthorize_error(e):
                        """Simple dbus error handler."""
                        logger.error(_("Error clearing token: %s") % str(e))

                  try:
                        def token_cleared():
                              """Do the next step."""
                              if self.__main:
                                    self.__main.check_for_token(True)

                        client = self.__bus.get_object(DBUS_IFACE_AUTH_NAME,
                            "/", follow_name_owner_changes=True)
                        iface = dbus.Interface(client, DBUS_IFACE_AUTH_NAME)
                        iface.clear_token(OAUTH_REALM, OAUTH_CONSUMER,
                                          reply_handler=token_cleared,
                                          error_handler=reauthorize_error)
                  except DBusException, e:
                        reauthorize_error(e)

            elif state == "UNKNOWN_ERROR":
                  # Disable some menu items
                  self.__litems["connect"].set_sensitive(False)
                  self.__litems["disconnect"].set_sensitive(False)
                  # Change the behavior to file a bug
                  if self.__fatal_error:
                        return

                  self.__fatal_error = True
                  self.__visible = True

                  # Pop up a notification
                  n = pynotify.Notification(
                        "Ubuntu One",
                        _("There was a fatal error in Ubuntu One. " +
                          "This may be a bug in the software. "
                          "Please click on the Ubuntu One icon " +
                          "in your panel to report a bug."))
                  pixbuf = self.__theme.load_icon(
                        "ubuntuone-client-error", NOTIFY_ICON_SIZE,
                        gtk.ICON_LOOKUP_GENERIC_FALLBACK)
                  n.set_icon_from_pixbuf(pixbuf)
                  n.set_urgency(pynotify.URGENCY_CRITICAL)
                  n.show()
                  # Set the tooltip and icon on the applet
                  self.set_tooltip(_("Fatal Error"))
                  self.set_from_icon_name("ubuntuone-client-error")

            else:
                  self.__connected = True
                  self.set_from_icon_name("ubuntuone-client-idle")
                  if state.startswith("CONNECTING") or \
                            state.startswith("START_CONNECTING") or \
                            state.startswith("AUTHENTICATING") or \
                            state.startswith("CONNECTED") or \
                            state.startswith("START_CONNECTED"):
                        self.set_from_icon_name("ubuntuone-client-idle")
                        self.set_tooltip(_("Connecting"))
                        self.__visible = True

            self.update_visibility()

            if self.__connected:
                  self.__litems["connect"].hide()
                  self.__litems["disconnect"].show()
            else:
                  self.__litems["connect"].show()
                  self.__litems["disconnect"].hide()
            self.__config.set("ubuntuone", "connected", self.__connected)

      def __hide_icon(self):
            """Timeout to hide tray icon after a period of inactivity."""
            if self.__show_when == 0:
                  return False

            self.__visible = False
            self.__visible_id = 0
            self.set_visible(False)
            return False

      def __get_root(self):
            """Method to get the rootdir from the sync daemon."""
            # Get the managed root directory
            try:
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
                                                 follow_name_owner_changes=True)
            except DBusException:
                  return False

            iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
            def got_root(root):
                  """We got the root dir."""
                  self.__managed_dir = root
                  if os.path.isdir(self.__managed_dir) and \
                            os.access(self.__managed_dir,
                                      os.F_OK | os.R_OK):
                        self.__ritems["open"].set_sensitive(True)
                        self.__add_to_places()
                  else:
                        self.__ritems["open"].set_sensitive(False)

            def got_err(error):
                  """Handle error from the dbus callback."""
                  self.sd_dbus_error(error)
                  self.__managed_dir = None
                  self.__ritems["open"].set_sensitive(False)

            iface.get_rootdir(reply_handler=got_root, error_handler=got_err)

            # Now get the current status
            try:
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/status",
                                                 follow_name_owner_changes=True)
                  iface = dbus.Interface(client, DBUS_IFACE_STATUS_NAME)
                  iface.current_status(reply_handler=self.__status_changed,
                                       error_handler=self.sd_dbus_error)
            except DBusException, e:
                  self.sd_dbus_error(e)
                  return False

            return False

      def __auth_finished(self, *args, **kwargs):
            """Got the oauth token, get the root and status."""
            gobject.idle_add(self.__get_root)

      def __build_menus(self):
            """Create the pop-up menu items."""
            # Create the left-click menu
            lmenu = gtk.Menu()

            self.__litems["status"] = gtk.MenuItem(
                  label=_("Your files are up to date."))
            lmenu.append(self.__litems["status"])
            self.__litems["status"].set_sensitive(False)
            self.__litems["status"].show()

            sep = gtk.SeparatorMenuItem()
            lmenu.append(sep)
            sep.show()

            self.__litems["connect"] = gtk.ImageMenuItem(
                  stock_id=gtk.STOCK_CONNECT)
            lmenu.append(self.__litems["connect"])
            self.__litems["connect"].connect("activate", self.__toggle_state)
            self.__litems["connect"].show()

            self.__litems["disconnect"] = gtk.ImageMenuItem(
                  stock_id=gtk.STOCK_DISCONNECT)
            lmenu.append(self.__litems["disconnect"])
            self.__litems["disconnect"].connect("activate", self.__toggle_state)

            lmenu.show()

            # Create the right-click menu
            rmenu = gtk.Menu()

            self.__ritems["bug"] = gtk.MenuItem(label=_("_Report a Problem"))
            rmenu.append(self.__ritems["bug"])
            self.__ritems["bug"].connect("activate", self.__report_problem)
            self.__ritems["bug"].show()

            self.__ritems["open"] = gtk.MenuItem(label=_("_Open Folder"))
            rmenu.append(self.__ritems["open"])
            self.__ritems["open"].connect("activate", self.__open_folder)
            self.__ritems["open"].set_sensitive(False)
            self.__ritems["open"].show()

            self.__ritems["web"] = gtk.MenuItem(label=_("_Go to Web"))
            rmenu.append(self.__ritems["web"])
            self.__ritems["web"].connect("activate", self.__open_website)
            self.__ritems["web"].show()

            self.__ritems["config"] = gtk.ImageMenuItem(
                  stock_id=gtk.STOCK_PREFERENCES)
            rmenu.append(self.__ritems["config"])
            self.__ritems["config"].connect("activate", self.__open_config)
            self.__ritems["config"].show()

            sep = gtk.SeparatorMenuItem()
            rmenu.append(sep)
            sep.show()

            self.__ritems["quit"] = gtk.ImageMenuItem(stock_id=gtk.STOCK_QUIT)
            rmenu.append(self.__ritems["quit"])
            self.__ritems["quit"].connect("activate", self.__quit_applet)
            self.__ritems["quit"].show()

            rmenu.show()

            return lmenu, rmenu

      def __size_changed(self, icon, size, data=None):
            """Callback for when the size changes."""
            if size < 24:
                  self.__size = 16
            elif size >= 24 and size < 32:
                  self.__size = 24
            elif size >= 32 and size < 48:
                  self.__size = 32
            elif size >= 48 and size < 64:
                  self.__size = 48
            else:
                  self.__size = size

      def __popup_menu(self, icon, button, timestamp, data=None):
            """Pops up the context menu for the tray icon."""
            if button == 0:
                  self.__status_menu.popup(None, None,
                                           gtk.status_icon_position_menu,
                                           button, timestamp, icon)
            else:
                  self.__config_menu.popup(None, None,
                                           gtk.status_icon_position_menu,
                                           button, timestamp, icon)

      def __stop_syncdaemon(self):
            """Tell the syncdaemon to quit."""
            def quit_error(e):
                  """Just log and ignore."""
                  logger.error(_("Quit Error: %s") % e.get_dbus_message())

            try:
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
                                                 follow_name_owner_changes=True)
                  iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
                  iface.quit(reply_handler=dbus_async,
                             error_handler=quit_error)
            except DBusException, e:
                  quit_error(e)

      def __quit_applet(self, menuitem, data=None):
            """Quit the daemon and closes the applet."""
            self.__stop_syncdaemon()

            from twisted.internet import reactor
            reactor.stop()

      def __toggle_state(self, menuitem, data=None):
            """Connects or disconnects the storage sync process."""
            try:
                  client = self.__bus.get_object(DBUS_IFACE_NAME, "/",
                                                 follow_name_owner_changes=True)
                  iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
                  if self.__connected:
                        iface.disconnect(reply_handler=dbus_async,
                                         error_handler=self.sd_dbus_error)
                  else:
                        if self.__main and self.__main.authorized is False:
                              self.__main.check_for_token(do_login=True)
                        iface.connect(reply_handler=dbus_async,
                                      error_handler=self.sd_dbus_error)
            except DBusException, e:
                  self.sd_dbus_error(e)

            self.__config.set("ubuntuone", "connected", not self.__connected)
            with open(CONF_FILE, "w+b") as f:
                  self.__config.write(f)

      def __open_folder(self, data=None):
            """Opens the storage folder in the file manager."""
            if not self.__managed_dir or not os.path.isdir(self.__managed_dir):
                  return

            folder = "file://%s" % quote(self.__managed_dir)
            do_xdg_open(folder)

      def __do_action(self, data=None):
            """Handles the most appropriate action when the icon is clicked."""
            if self.__fatal_error:
                  self.__report_problem()
                  self.__quit_applet(None)
                  return

            if self.__need_update:
                  do_xdg_open("apt:ubuntuone-storage-protocol?refresh=yes")
                  return

            # Popup the status menu
            self.emit("popup-menu", 0, gtk.get_current_event_time())

      def __report_problem_real(self):
            """Runs apport to report a problem against our code."""
            args = ["ubuntu-bug", "ubuntuone-client"]
            ret = subprocess.call(args, env=os.environ)
            if ret != 0:
                  logger.error(_("Failed to run 'ubuntu-bug'"))

      def __report_problem(self, data=None):
            """Pops another thread to run apport in."""
            Thread(target=self.__report_problem_real, name="apport").start()

      def __open_website(self, data=None, url=None):
            """Opens the one.ubuntu.com web site."""
            if url:
                  do_xdg_open(url)
            else:
                  do_xdg_open("https://one.ubuntu.com/")


      def __open_config(self, data=None):
            """Opens the preferences dialog."""
            do_config_open()

      def __add_to_places(self):
            """Add the managed directory to the .gtk-bookmarks file."""
            # Only add once
            if self.__config.getboolean("ubuntuone", "bookmarked"):
                  return

            path = os.path.join(os.path.expanduser("~"), ".gtk-bookmarks")
            with open(path, "a+") as f:
                  bookmarks_entry = "file://%s %s\n" % (
                        quote(self.__managed_dir), BOOKMARK_NAME)
                  in_file = False
                  for line in f:
                        if line == bookmarks_entry:
                              in_file = True
                  if not in_file:
                        f.write(bookmarks_entry)

            self.__config.set("ubuntuone", "bookmarked", "True")
            with open(CONF_FILE, "w+b") as f:
                  self.__config.write(f)

      def sd_dbus_error(self, error):
            """
            Handle DBus errors for crucial syncdaemon calls,
            and change the applet behavior slightly.
            """
            logger.error(_("DBus Error: %s") % error.get_dbus_message())
            if self.__fatal_error:
                  return

            self.__fatal_error = True
            self.__status_changed({'name' : 'UNKNOWN_ERROR'})


class AppletConfig(dbus.service.Object):
      """DBus Service object"""

      def __init__(self, icon, *args, **kwargs):
            """Initialize our magic."""
            self.icon = icon
            self.path = "/config"
            self.bus = dbus.SessionBus()
            bus_name = dbus.service.BusName(APPLET_BUS_NAME,
                                            bus=self.bus)
            dbus.service.Object.__init__(self, bus_name=bus_name,
                                         object_path=self.path)

      @dbus.service.method(APPLET_CONFIG_NAME,
                           in_signature='i', out_signature='')
      def set_visibility_config(self, visibility):
            self.icon.set_visibility_config(visibility)

      @dbus.service.method(APPLET_CONFIG_NAME,
                           in_signature='i', out_signature='')
      def set_connection_config(self, connect):
            self.icon.set_connection_config(connect)


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

      # Register DBus service for making sure we run only one instance
      bus = dbus.SessionBus()
      if bus.request_name(APPLET_BUS_NAME, dbus.bus.NAME_FLAG_DO_NOT_QUEUE) == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
            print _("Ubuntu One client applet already running, quitting")
            do_config_open()
            sys.exit(0)

      gtk.rc_parse_string(RCSTYLE)

      icon = AppletMain()
      icon.main()
