3
# ubuntuone-client-applet - Tray icon applet for managing Ubuntu One
5
# Author: Rodney Dawes <rodney.dawes@canonical.com>
7
# Copyright 2009 Canonical Ltd.
9
# This program is free software: you can redistribute it and/or modify it
10
# under the terms of the GNU General Public License version 3, as published
11
# by the Free Software Foundation.
13
# This program is distributed in the hope that it will be useful, but
14
# WITHOUT ANY WARRANTY; without even the implied warranties of
15
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
16
# PURPOSE. See the GNU General Public License for more details.
18
# You should have received a copy of the GNU General Public License along
19
# with this program. If not, see <http://www.gnu.org/licenses/>.
21
from __future__ import with_statement
30
from gettext import gettext as _
34
# pylint: disable-msg=F0401
37
from dbus.exceptions import DBusException
38
from dbus.mainloop.glib import DBusGMainLoop
39
from ubuntuone.oauthdesktop.main import Login
40
from gobject.option import OptionGroup, OptionParser, make_option
41
from xdg.BaseDirectory import xdg_data_home, xdg_config_home
42
from urllib import quote
44
from ubuntuone.oauthdesktop.logger import setupLogging
47
logger = logging.getLogger("UbuntuOne.Client.Applet")
49
DBusGMainLoop(set_as_default=True)
51
# Wait for 30 seconds to get a proper login request
54
# Prepend tooltip labels with this label
55
TOOLTIP_TITLE = "Ubuntu One: "
57
DBUS_IFACE_NAME = "com.ubuntuone.SyncDaemon"
58
DBUS_IFACE_SYNC_NAME = "com.ubuntuone.SyncDaemon.SyncDaemon"
59
DBUS_IFACE_STATUS_NAME = "com.ubuntuone.SyncDaemon.Status"
61
def dbus_error(error):
62
"""Got an error from DBus."""
63
logger.debug("DBusError: %s" % error.get_dbus_message())
65
def dbus_async(*args):
66
"""Simple handler to make dbus do stuff async."""
69
class AppletMain(object):
70
"""Main applet process class."""
72
def __init__(self, signup=False, *args, **kw):
73
"""Initializes the child threads and dbus monitor."""
74
from twisted.internet import gtk2reactor
76
login = Login(dbus.service.BusName("com.ubuntuone.Authentication",
77
bus=dbus.SessionBus()))
79
self.__signup = signup
83
self.__bus = dbus.SessionBus()
84
self.__bus.add_signal_receiver(
85
handler_function=self.__new_credentials,
86
signal_name="NewCredentials",
87
dbus_interface="com.ubuntuone.Authentication")
88
self.__bus.add_signal_receiver(
89
handler_function=self.__auth_denied,
90
signal_name="AuthorizationDenied",
91
dbus_interface="com.ubuntuone.Authentication")
93
def __new_credentials(self, realm=None, consumer_key=None, sender=None):
94
"""Signal callback for when we get new credentials."""
95
self.__start_storage_daemon()
98
self.__icon = AppletIcon()
99
if self.__check_id > 0:
100
gobject.source_remove(self.__check_id)
102
self.add_to_autostart()
104
def __auth_denied(self):
105
"""Signal callback for when auth was denied by user."""
107
"""Do the quit on a timeout"""
108
from twisted.internet import reactor
112
self.remove_from_autostart()
113
if not self.__signup:
114
self.__check_id = gobject.timeout_add_seconds(LOGIN_TIMEOUT,
117
def __check_for_token(self):
118
"""Method to check for an existing token."""
120
client = self.__bus.get_object("com.ubuntuone.Authentication",
122
except DBusException:
125
iface = dbus.Interface(client, "com.ubuntuone.Authentication")
126
iface.maybe_login("https://ubuntuone.com", "ubuntuone",
128
reply_handler=dbus_async,
129
error_handler=dbus_error)
132
def __start_storage_daemon_maybe(self):
133
"""Start the storage daemon."""
135
client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
136
except DBusException:
139
iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
140
iface.get_rootdir(reply_handler=dbus_async,
141
error_handler=dbus_error)
144
def __start_storage_daemon(self):
145
"""Need to call dbus from a idle callback"""
146
gobject.idle_add(self.__start_storage_daemon_maybe)
148
def add_to_autostart(self):
149
"""Add ourself to the autostart config."""
150
autostart_entry = """[Desktop Entry]
152
Comment=Control applet for Ubuntu One
153
Exec=ubuntuone-client-applet
154
Icon=ubuntuone-client
157
X-Ubuntu-Gettext-Domain=ubuntuone-client
158
X-KDE-autostart-after=panel
159
X-GNOME-Autostart-enabled=true
161
if not os.path.exists(os.path.join(xdg_config_home, "autostart")):
162
os.makedirs(os.path.join(xdg_config_home, "autostart"))
164
with open(os.path.join(xdg_config_home, "autostart",
165
"ubuntuone-client-applet.desktop"),
167
f.write(autostart_entry)
169
def remove_from_autostart(self):
170
"""Remove ourself from the autostart config."""
171
path = os.path.join(xdg_config_home, "autostart",
172
"ubuntuone-client-applet.desktop")
179
"""Starts the gtk main loop."""
180
from twisted.internet import reactor
181
gobject.idle_add(self.__check_for_token)
186
def do_xdg_open(path_or_url):
187
"""Utility method to run xdg-open with path_or_url."""
188
ret = subprocess.call(["xdg-open", path_or_url])
190
logger.debug("Failed to run 'xdg-open %s'" % path_or_url)
192
class AppletIcon(gtk.StatusIcon):
194
Custom StatusIcon derived from gtk.StatusIcon which supports
195
animated icons and a few other nice things.
198
def __init__(self, *args, **kw):
199
"""Initializes our custom StatusIcon based widget."""
200
super(AppletIcon, self).__init__(*args, **kw)
202
self.__managed_dir = None
204
self.__theme = gtk.icon_theme_get_default()
205
iconpath = os.path.abspath(os.path.join(
206
os.path.split(os.path.dirname(__file__))[0],
208
self.__theme.append_search_path(iconpath)
210
self.__theme.append_search_path(os.path.sep + os.path.join(
211
"usr", "share", "ubuntuone-client", "icons"))
212
self.__theme.append_search_path(os.path.sep + os.path.join(
213
"usr", "local", "share", "ubuntuone-client", "icons"))
215
self.set_from_icon_name('ubuntuone-client')
216
self.connect("popup-menu", self.__popup_menu)
217
self.connect("activate", self.__open_folder)
230
self.__size_changed(self, self.__size)
233
self.__menu = self.__build_menus()
235
self.__connected = False
236
self.__need_update = False
238
pynotify.init("Ubuntu One")
240
self.__bus = dbus.SessionBus()
242
self.__bus.add_signal_receiver(
243
handler_function=self.__status_changed,
244
signal_name="StatusChanged",
245
dbus_interface=DBUS_IFACE_STATUS_NAME)
247
gobject.idle_add(self.__get_root)
249
def __status_changed(self, status):
250
"""The sync daemon status changed."""
251
if self.__managed_dir is None:
252
gobject.idle_add(self.__get_root)
253
if status['name'] == "OFFLINE" or \
254
status['name'].startswith("INIT") or \
255
status['name'].startswith("READY"):
257
self.set_from_icon_name("ubuntuone-client-offline")
258
self.set_tooltip(TOOLTIP_TITLE + _("Disconnected"))
259
self.__connected = False
260
elif status['name'].startswith("IDLE"):
262
self.set_from_icon_name("ubuntuone-client")
263
self.set_tooltip(TOOLTIP_TITLE + _("Idle"))
264
self.__connected = True
265
elif status['name'] == "BAD_VERSION":
267
self.set_from_icon_name("ubuntuone-client-offline")
268
self.set_tooltip(TOOLTIP_TITLE + _("Update Required"))
269
self.__connected = False
270
self.__need_update = True
271
self.__items["connect"].set_sensitive(False)
272
n = pynotify.Notification(
274
_("A new client version is required to continue " +
275
"using the service. Please click the Ubuntu One" +
276
"applet icon to upgrade."),
277
"ubuntuone-client-offline")
278
n.set_urgency(pynotify.URGENCY_CRITICAL)
280
logger.debug("Invalid protocol version. Update needed.")
281
elif status['name'] == "AUTH_FAILED":
283
self.set_from_icon_name("ubuntuone-client-offline")
284
self.set_tooltip(TOOLTIP_TITLE + "Authentication failed")
285
self.__connected = False
287
client = self.__bus.get_object(
288
"com.ubuntuone.Authentication",
290
iface = dbus.Interface(client,
291
"com.ubuntuone.Authentication")
292
iface.maybe_login("https://ubuntuone.com",
295
reply_handler=dbus_async,
296
error_handler=dbus_error)
298
except DBusException, e:
303
self.__connected = True
304
if status['name'].startswith("CONNECTING") or \
305
status['name'].startswith("AUTHENTICATING") or \
306
status['name'].startswith("CONNECTED"):
307
self.set_tooltip(TOOLTIP_TITLE + _("Connecting"))
308
elif status['name'].startswith("SCANNING") or \
309
status['name'].startswith("READING"):
310
self.set_tooltip(TOOLTIP_TITLE + _("Scanning"))
311
elif status['name'].startswith("WORKING"):
312
self.set_tooltip(TOOLTIP_TITLE + _("Synchronizing"))
313
elif status['name'].startswith("UNKNOWN_ERROR"):
316
self.set_tooltip(TOOLTIP_TITLE + _("Working"))
319
self.__items["connect"].hide()
320
self.__items["disconnect"].show()
322
self.__items["connect"].show()
323
self.__items["disconnect"].hide()
325
def __get_root(self):
326
"""Method to get the rootdir from the sync daemon."""
327
# Get the managed root directory
329
client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
330
except DBusException:
333
iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
335
"""We got the root dir."""
336
if self.__managed_dir:
337
self.__remove_from_places(self.__managed_dir)
338
self.__managed_dir = root
339
if os.path.isdir(self.__managed_dir) and \
340
os.access(self.__managed_dir,
342
self.__items["open"].set_sensitive(True)
343
self.__add_to_places()
345
self.__items["open"].set_sensitive(False)
348
"""Handle error from the dbus callback."""
349
if self.__managed_dir:
350
self.__remove_from_places(self.__managed_dir)
351
self.__managed_dir = None
352
self.__items["open"].set_sensitive(False)
354
iface.get_rootdir(reply_handler=got_root, error_handler=got_err)
356
# Now get the current status
358
client = self.__bus.get_object(DBUS_IFACE_NAME, "/status")
359
except DBusException:
362
iface = dbus.Interface(client, DBUS_IFACE_STATUS_NAME)
363
iface.current_status(reply_handler=self.__status_changed,
364
error_handler=dbus_error)
368
def __build_menus(self):
369
"""Create the pop-up menu items."""
372
self.__items["connect"] = gtk.ImageMenuItem(stock_id=gtk.STOCK_CONNECT)
373
menu.append(self.__items["connect"])
374
self.__items["connect"].connect("activate", self.__toggle_state)
375
self.__items["connect"].show()
377
self.__items["disconnect"] = gtk.ImageMenuItem(stock_id=gtk.STOCK_DISCONNECT)
378
menu.append(self.__items["disconnect"])
379
self.__items["disconnect"].connect("activate", self.__toggle_state)
381
sep = gtk.SeparatorMenuItem()
385
self.__items["bug"] = gtk.MenuItem(label=_("_Report a Problem"))
386
menu.append(self.__items["bug"])
387
self.__items["bug"].connect("activate", self.__open_website,
388
"https://bugs.launchpad.net/ubuntuone-client/+filebug")
389
self.__items["bug"].show()
391
self.__items["open"] = gtk.MenuItem(label=_("_Open Folder"))
392
menu.append(self.__items["open"])
393
self.__items["open"].connect("activate", self.__open_folder)
394
self.__items["open"].set_sensitive(False)
395
self.__items["open"].show()
397
self.__items["web"] = gtk.MenuItem(label=_("_Go to Web"))
398
menu.append(self.__items["web"])
399
self.__items["web"].connect("activate", self.__open_website)
400
self.__items["web"].show()
402
sep = gtk.SeparatorMenuItem()
406
self.__items["quit"] = gtk.ImageMenuItem(stock_id=gtk.STOCK_QUIT)
407
menu.append(self.__items["quit"])
408
self.__items["quit"].connect("activate", self.__quit_applet)
409
self.__items["quit"].show()
415
def __size_changed(self, icon, size, data=None):
416
"""Callback for when the size changes."""
419
elif size >= 24 and size < 32:
421
elif size >= 32 and size < 48:
423
elif size >= 48 and size < 64:
428
self.__pixbuf = self.__theme.load_icon(
429
'ubuntuone-client-working',
431
gtk.ICON_LOOKUP_GENERIC_FALLBACK)
433
self.__hframes = self.__pixbuf.get_width() / self.__size
434
self.__vframes = self.__pixbuf.get_height() / self.__size
439
def __spinner_timeout(self):
440
"""Timeout callback that spins the spinner."""
445
# Skip the first (resting) frame to avoid flicker
446
if self.__hpos == 0 and self.__vpos == 0:
449
x = self.__size * self.__hpos
450
y = self.__size * self.__vpos
451
pixbuf = self.__pixbuf.subpixbuf(x, y, self.__size, self.__size)
452
self.set_from_pixbuf(pixbuf)
455
if self.__hpos == self.__hframes:
458
if self.__vpos >= self.__vframes:
463
def set_busy(self, busy=True):
464
if self.__busy and busy:
468
gobject.source_remove(self.__busy_id)
471
self.__busy_id = gobject.timeout_add(100,
472
self.__spinner_timeout)
474
def __popup_menu(self, icon, button, timestamp, data=None):
475
"""Pops up the context menu for the tray icon."""
476
self.__menu.popup(None, None, gtk.status_icon_position_menu,
477
button, timestamp, icon)
479
def __quit_applet(self, menuitem, data=None):
480
"""Quit the daemon and closes the applet."""
483
client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
484
iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
485
iface.quit(reply_handler=dbus_async,
486
error_handler=dbus_error)
487
except DBusException:
490
from twisted.internet import reactor
493
def __toggle_state(self, menuitem, data=None):
494
"""Connects or disconnects the storage sync process."""
496
client = self.__bus.get_object(DBUS_IFACE_NAME, "/")
497
except DBusException:
500
iface = dbus.Interface(client, DBUS_IFACE_SYNC_NAME)
502
iface.disconnect(reply_handler=dbus_async,
503
error_handler=dbus_error)
505
iface.connect(reply_handler=dbus_async,
506
error_handler=dbus_error)
508
def __open_folder(self, data=None):
509
"""Opens the storage folder in the file manager."""
510
if self.__need_update:
511
do_xdg_open("apt:ubuntuone-storage-protocol?refresh=yes")
514
if not self.__managed_dir or not os.path.isdir(self.__managed_dir):
517
folder = "file://%s" % quote(self.__managed_dir)
520
def __open_website(self, data=None, url=None):
521
"""Opens the ubuntuone.com web site."""
525
do_xdg_open("https://ubuntuone.com/")
527
def __add_to_places(self):
528
"""Add the managed directory to the .gtk-bookmarks file."""
529
path = os.path.join(os.path.expanduser("~"), ".gtk-bookmarks")
530
with open(path, "a+") as f:
531
bookmarks_entry = "file://%s %s\n" % (
532
quote(self.__managed_dir),
533
os.path.basename(self.__managed_dir))
536
if line == bookmarks_entry:
539
f.write(bookmarks_entry)
541
def __remove_from_places(self, dir):
542
"""Remove the old path from the .gtk-bookmarks file."""
543
path = os.path.join(os.path.expanduser("~"), ".gtk-bookmarks")
544
with open(path, "a+") as f:
545
entry = "file://%s %s\n" % (quote(dir), os.path.basename(dir))
552
output = "".join(lines)
556
if __name__ == "__main__":
557
parser = OptionParser("",
558
description="Ubuntu One status applet",
560
make_option("--signup", "-s",
563
help=_("Log in or Sign Up for Ubuntu One")),
565
options, args = parser.parse_args()
567
# Register DBus service for making sure we run only one instance
568
bus = dbus.SessionBus()
569
if bus.request_name("com.ubuntuone.ClientApplet", dbus.bus.NAME_FLAG_DO_NOT_QUEUE) == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
570
print _("Ubuntu One client applet already running, quitting")
573
icon = AppletMain(signup=options.signup)