2
# -*- coding: utf-8 -*-
4
# ubuntuone-client-applet - Tray icon applet for managing Ubuntu One
6
# Authors: Rodney Dawes <rodney.dawes@canonical.com>
7
# Rodrigo Moya <rodrigo.moya@canonical.com>
9
# Copyright 2009-2010 Canonical Ltd.
11
# This program is free software: you can redistribute it and/or modify it
12
# under the terms of the GNU General Public License version 3, as published
13
# by the Free Software Foundation.
15
# This program is distributed in the hope that it will be useful, but
16
# WITHOUT ANY WARRANTY; without even the implied warranties of
17
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
18
# PURPOSE. See the GNU General Public License for more details.
20
# You should have received a copy of the GNU General Public License along
21
# with this program. If not, see <http://www.gnu.org/licenses/>.
23
from __future__ import division, with_statement
34
from multiprocessing import Pipe, Process
35
from threading import Thread
36
from oauth import oauth
38
from ubuntu_sso import (DBUS_BUS_NAME, DBUS_CREDENTIALS_IFACE,
39
DBUS_CREDENTIALS_PATH)
40
from ubuntu_sso.credentials import (HELP_TEXT_KEY, PING_URL_KEY, TC_URL_KEY,
43
from ubuntuone import clientdefs
44
from ubuntuone.platform.linux.tools import SyncDaemonTool
45
from ubuntuone.logger import (basic_formatter, LOGFOLDER,
46
CustomRotatingFileHandler)
51
from dbus.exceptions import DBusException
52
from dbus.mainloop.glib import DBusGMainLoop
54
LOG_LEVEL = logging.INFO
56
logger = logging.getLogger("ubuntuone-preferences")
57
logger.setLevel(LOG_LEVEL)
58
handler = CustomRotatingFileHandler(filename=os.path.join(LOGFOLDER,
60
handler.setFormatter(basic_formatter)
61
handler.setLevel(LOG_LEVEL)
62
logger.addHandler(handler)
67
# Try importing the Ubuntu One desktopcouch enablement api
69
from desktopcouch.application.replication_services import (
72
logger.error("DesktopCouch replication API not found")
74
DBusGMainLoop(set_as_default=True)
76
DBUS_IFACE_NAME = "com.ubuntuone.SyncDaemon"
77
DBUS_IFACE_CONFIG_NAME = DBUS_IFACE_NAME + ".Config"
78
DBUS_IFACE_STATUS_NAME = DBUS_IFACE_NAME + ".Status"
80
# Our own DBus interface
81
PREFS_BUS_NAME = "com.ubuntuone.Preferences"
83
# This is where thy music lies
84
U1MSPATH = os.path.expanduser("~/.ubuntuone/Purchased from Ubuntu One")
86
# Why thank you GTK+ for enforcing style-set and breaking API
89
GtkDialog::action-area-border = 12
90
GtkDialog::button-spacing = 6
91
GtkDialog::content-area-border = 0
93
widget_class '*Dialog*' style 'dialogs'
96
# Some defines for the bindwood installation magic
97
BW_PKG_NAME = "xul-ext-bindwood"
98
BW_INST_ARGS = ['apturl', 'apt:%s?section=universe' % BW_PKG_NAME]
99
BW_CHCK_ARGS = ['dpkg', '-l', BW_PKG_NAME]
101
# This is a global so we can avoid creating multiple instances in some cases
103
oauth_consumer = None
105
def dbus_async(*args, **kwargs):
106
"""Simple handler to make dbus do stuff async."""
110
def do_login_request(bus, error_handler):
111
"""Make a login request to the login handling daemon."""
113
client = bus.get_object(DBUS_BUS_NAME,
114
DBUS_CREDENTIALS_PATH,
115
follow_name_owner_changes=True)
116
iface = dbus.Interface(client, DBUS_CREDENTIALS_IFACE)
117
params = {HELP_TEXT_KEY: clientdefs.DESCRIPTION,
118
TC_URL_KEY: clientdefs.TC_URL,
119
PING_URL_KEY: clientdefs.PING_URL,
121
iface.register(clientdefs.APP_NAME, params,
122
reply_handler=dbus_async,
123
error_handler=error_handler)
124
except DBusException, e:
128
def really_do_rest_request(url, method, conn):
129
"""Second-order helper that does the REST request.
131
Necessary because of libproxy's orneriness WRT threads: LP:633241.
133
from ubuntuone.api.restclient import RestClient
134
logger.debug("really_do_rest_request (%s:%s)", method, url)
135
rest_client = RestClient(url)
136
result = rest_client.call(url, method, oauth_consumer, oauth_token)
137
logger.debug("got result for really_do_rest_request (%s:%s)", method, url)
139
logger.debug("end really_do_rest_request (%s:%s)", method, url)
142
def do_rest_request(proc, conn, callback):
143
"""Helper that handles the REST request."""
145
logger.debug("do_rest_request (%s)", pid)
149
logger.debug("got result for do_rest_request (%d)", pid)
150
if callback is not None:
153
logger.debug("end do_rest_request (%d)", pid)
156
def make_rest_request(url=None, method='GET', callback=None):
157
"""Helper that makes an oauth-wrapped REST request."""
158
logger.debug("make_rest_request (%s:%s)", method, url)
159
conn1, conn2 = Pipe(False)
160
p = Process(target=really_do_rest_request, args=(url, method, conn2))
162
logger.debug("make_rest_request (%s:%s) started %d", method, url, p.pid)
163
Thread(target=do_rest_request, args=(p, conn1, callback)).start()
164
logger.debug("end make_rest_request (%s:%s)", method, url)
167
class DevicesWidget(gtk.Table):
173
url='https://one.ubuntu.com/api/1.0/devices/'):
174
super(DevicesWidget, self).__init__(rows=2, columns=3)
176
self.sdtool = SyncDaemonTool(bus)
177
self.set_border_width(6)
178
self.set_row_spacings(6)
179
self.set_col_spacings(6)
184
self.table_widgets = []
186
self.connected = None # i.e. unknown
191
self.bw_limited = False
192
self.up_limit = 2097152
193
self.dn_limit = 2097152
194
self.got_limits = False
198
self.status_label = gtk.Label("")
199
self.attach(self.status_label, 0, 3, 2, 3)
201
self.description = gtk.Label(_("The devices connected to your personal"
202
" cloud network are listed below"))
203
self.description.set_alignment(0., .5)
204
self.description.set_line_wrap(True)
205
self.attach(self.description, 0, 3, 0, 1, xpadding=12, ypadding=12)
207
def update_bw_settings(self):
209
Push the bandwidth settings at syncdaemon.
212
if self.sdtool.is_files_sync_enabled():
213
self.sdtool.set_throttling_limits(self.dn_limit or -1,
215
self.sdtool.enable_throttling(self.bw_limited)
217
def handle_bw_controls_changed(self, *a):
219
Sync the bandwidth throttling model with the view.
221
Start a timer to sync with syncdaemon too.
223
if not self.sdtool.is_files_sync_enabled():
225
# Remove the timeout ...
226
if self._update_id != 0:
227
gobject.source_remove(self._update_id)
230
self.bw_limited = self.bw_chk.get_active()
231
self.up_limit = self.up_spin.get_value_as_int() * 1024
232
self.dn_limit = self.dn_spin.get_value_as_int() * 1024
234
# ... and add the timeout back
235
self._update_id = gobject.timeout_add_seconds(
236
1, self.update_bw_settings)
238
def handle_bw_checkbox_toggled(self, checkbox, *widgets):
240
Callback for the bandwidth togglebutton.
242
active = checkbox.get_active()
243
for widget in widgets:
244
widget.set_sensitive(active)
245
self.handle_bw_controls_changed()
247
def handle_limits(self, limits):
249
Callback for when syncdaemon tells us its throttling limits.
251
self.got_limits = True
252
self.up_limit = int(limits['upload'])
253
self.dn_limit = int(limits['download'])
254
if self.up_spin is not None and self.dn_spin is not None:
255
self.up_spin.set_value(self.up_limit / 1024)
256
self.dn_spin.set_value(self.dn_limit / 1024)
258
# Now we can connect to spin buttons' signals
259
self.up_spin.connect("value-changed", self.handle_bw_controls_changed)
260
self.dn_spin.connect("value-changed", self.handle_bw_controls_changed)
262
def handle_throttling_enabled(self, enabled):
264
Callback for when syncdaemon tells us whether throttling is enabled.
266
self.bw_limited = enabled
267
if self.bw_chk is not None:
268
self.bw_chk.set_active(enabled)
270
def handle_state_change(self, new_state):
272
Callback for when syncdaemon's state changes.
274
if new_state['is_error']:
275
# this syncdaemon isn't going to connect no more
276
self.connected = None
278
self.connected = new_state['is_connected']
279
if self.conn_btn is not None:
281
self.conn_btn.set_label(_("Disconnect"))
283
self.conn_btn.set_label(_("Connect"))
284
if self.connected is None:
285
self.conn_btn.set_sensitive(False)
287
self.conn_btn.set_sensitive(True)
289
def error(self, msg):
291
Clear the table and show the error message in its place.
293
This might be better as an error dialog.
296
dialog = gtk.MessageDialog(self.get_toplevel(),
297
gtk.DIALOG_DESTROY_WITH_PARENT |
299
gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE,
301
dialog.format_secondary_text(str(msg))
306
while gtk.events_pending():
309
def get_devices(self):
311
Ask the server for a list of devices
313
Hook up parse_devices to run on the result (when it gets here).
315
make_rest_request(url=self.base_url, callback=self.parse_devices)
317
def parse_devices(self, result):
319
Parse the list of devices, and hook up list_devices if it worked.
322
if result and isinstance(result, list):
323
self.devices = result
324
elif isinstance(result, dict):
325
error = result.get('error', None)
326
if not error and result.get('status', None):
327
error = result.get('reason', None)
329
error = "Invalid response getting devices list."
331
error = "Got empty result for devices list."
337
gobject.idle_add(self.list_devices)
339
def clear_devices_view(self):
341
Clear out almost all the widgets.
343
All except from the table, the description and the
344
status_label get destroyed.
346
for i in self.get_children():
347
if i not in (self.description, self.status_label):
354
def list_devices(self):
356
Populate the table with the list of devices.
358
If the list of devices is empty, make a fake one that refers
359
to the local machine (to get the connect/restart buttons).
361
self.clear_devices_view()
363
fsync_enabled = self.sdtool.is_files_sync_enabled()
367
# a stopgap device so you can at least try to connect
368
self.devices = [{'kind': 'Computer',
369
'description': _("<LOCAL MACHINE>"),
370
'token': oauth_token.key if oauth_token else '',
373
self.resize(len(self.devices)+1, 3)
375
self.status_label.set_label("")
378
for row in self.devices or ():
381
img.set_from_icon_name(row['kind'].lower(), gtk.ICON_SIZE_DND)
382
desc = gtk.Label(row['description'])
383
desc.set_alignment(0., .5)
384
self.attach(img, 0, 1, i, i+1)
385
self.attach(desc, 1, 2, i, i+1)
386
if 'FAKE' not in row:
387
# we don't include the "Remove" button for the fake entry :)
388
butn = gtk.Button(_("Remove"))
389
butn.connect('clicked', self.remove,
390
row['kind'], row.get('token'))
391
self.attach(butn, 2, 3, i, i+1, xoptions=0, yoptions=0)
392
if ((oauth_token and row.get('token') == oauth_token.key or 'FAKE' in row)
394
self.bw_chk = ck_btn = gtk.CheckButton(
395
_("_Limit bandwidth"))
396
ck_btn.set_active(self.bw_limited)
397
up_lbl = gtk.Label(_("Maximum _upload speed (KB/s):"))
398
up_lbl.set_alignment(0., .5)
399
up_lbl.set_use_underline(True)
400
adj = gtk.Adjustment(value=self.up_limit/1024.,
401
lower=0.0, upper=4096.0,
402
step_incr=1.0, page_incr=16.0)
403
self.up_spin = up_btn = gtk.SpinButton(adj)
404
up_lbl.set_mnemonic_widget(up_btn)
405
dn_lbl = gtk.Label(_("Maximum _download speed (KB/s):"))
406
dn_lbl.set_alignment(0., .5)
407
dn_lbl.set_use_underline(True)
408
adj = gtk.Adjustment(value=self.dn_limit/1024.,
409
lower=0.0, upper=4096.0,
410
step_incr=1.0, page_incr=16.0)
411
self.dn_spin = dn_btn = gtk.SpinButton(adj)
412
dn_lbl.set_mnemonic_widget(dn_btn)
413
ck_btn.connect('toggled', self.handle_bw_checkbox_toggled,
414
up_lbl, up_btn, dn_lbl, dn_btn)
416
for w in up_lbl, up_btn, dn_lbl, dn_btn:
417
w.set_sensitive(self.bw_limited)
419
self.conn_btn = gtk.Button(_("Connect"))
420
if self.connected is None:
421
self.conn_btn.set_sensitive(False)
423
self.conn_btn.set_label(_("Disconnect"))
424
self.conn_btn.connect('clicked', self.handle_connect_button)
425
restart_btn = gtk.Button(_("Restart"))
426
restart_btn.connect('clicked', self.handle_restart_button)
427
btn_box = gtk.HButtonBox()
428
btn_box.add(self.conn_btn)
429
btn_box.add(restart_btn)
432
self.attach(ck_btn, 1, 3, i, i+1)
434
self.attach(up_lbl, 1, 2, i, i+1)
435
self.attach(up_btn, 2, 3, i, i+1)
437
self.attach(dn_lbl, 1, 2, i, i+1)
438
self.attach(dn_btn, 2, 3, i, i+1)
440
self.attach(btn_box, 1, 3, i, i+1)
444
def handle_connect_button(self, *a):
446
Callback for the Connect/Disconnect button.
448
self.conn_btn.set_sensitive(False)
450
self.sdtool.disconnect()
452
self.sdtool.connect()
454
def handle_restart_button(self, *a):
456
Callback for the Restart button.
458
self.sdtool.quit().addCallbacks(lambda _: self.sdtool.start())
460
def remove(self, button, kind, token):
462
Callback for the Remove button.
464
Starts an async request to remove a device.
466
make_rest_request(url=('%sremove/%s/%s' % (self.base_url,
467
kind.lower(), token)),
468
callback=self.parse_devices)
469
def local_removal_cb(*args, **kwargs):
470
"""Try to get a new token if we remove the local one."""
471
do_login_request(self.bus, self.error)
473
if token == oauth_token.key:
475
client = self.bus.get_object(DBUS_BUS_NAME,
476
DBUS_CREDENTIALS_PATH,
477
follow_name_owner_changes=True)
478
iface = dbus.Interface(client, DBUS_CREDENTIALS_IFACE)
479
iface.clear_credentials(clientdefs.APP_NAME, {},
480
reply_handler=local_removal_cb,
481
error_handler=self.error)
482
except DBusException, e:
486
class UbuntuOneDialog(gtk.Dialog):
487
"""Preferences dialog."""
489
def __init__(self, config=None, *args, **kw):
490
"""Initializes our config dialog."""
491
super(UbuntuOneDialog, self).__init__(*args, **kw)
492
self.set_title(_("Ubuntu One Preferences"))
493
self.set_has_separator(False)
494
self.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
495
self.set_default_response(gtk.RESPONSE_CLOSE)
496
self.set_icon_name("ubuntuone")
497
self.set_default_size(400, 300)
499
self.connect("close", self.__handle_response, gtk.RESPONSE_CLOSE)
500
self.connect("response", self.__handle_response)
502
# desktopcouch ReplicationExclusion, or None
505
# folder id for dealing with the Music folder
508
self.__bus = dbus.SessionBus()
510
# Timeout ID to avoid spamming DBus from spinbutton changes
517
self.sdtool = SyncDaemonTool(self.__bus)
518
if self.sdtool.is_files_sync_enabled():
521
self.status_label.set_text(_("Disconnected"))
523
logger.info("Starting Ubuntu One Preferences")
525
def _hookup_dbus(self):
529
self.sdtool.get_status().addCallbacks(self.__got_state,
533
self.__bus.add_signal_receiver(
534
handler_function=self.__got_state,
535
signal_name='StatusChanged',
536
dbus_interface=DBUS_IFACE_STATUS_NAME)
539
client = self.__bus.get_object(DBUS_IFACE_NAME, "/config",
540
follow_name_owner_changes=True)
541
iface = dbus.Interface(client, DBUS_IFACE_CONFIG_NAME)
542
iface.get_throttling_limits(
543
reply_handler=self.__got_limits,
544
error_handler=self.__dbus_error)
545
iface.bandwidth_throttling_enabled(
546
reply_handler=self.__got_enabled,
547
error_handler=self.__dbus_error)
548
except DBusException, e:
552
def __dbus_error(self, error):
553
"""Error getting throttling config."""
556
def __sd_error(self, error):
557
"""Error talking to syncdaemon."""
560
def __got_state(self, state):
561
"""Got the state of syncdaemon."""
562
if not state['is_connected']:
563
self.status_label.set_text(_("Disconnected"))
566
status = state['name']
567
queues = state['queues']
571
if status == u'QUEUE_MANAGER' and queues == u'IDLE':
572
self.status_label.set_text(_("Synchronization complete"))
574
self.status_label.set_text(_(u"Synchronization in progress…"))
576
# Update the stuff in the devices tab
577
self.devices.handle_state_change(state)
579
def __got_limits(self, limits):
580
"""Got the throttling limits."""
581
logger.debug("got limits: %s" % (limits,))
582
self.devices.handle_limits(limits)
584
def __got_enabled(self, enabled):
585
"""Got the throttling enabled config."""
586
self.devices.handle_throttling_enabled(enabled)
588
def __handle_response(self, dialog, response):
589
"""Handle the dialog's response."""
591
while gtk.events_pending():
593
self.devices.handle_bw_controls_changed()
594
gobject.timeout_add_seconds(5, gtk.main_quit)
596
def _format_for_gb_display(self, bytes):
597
"""Format bytes into reasonable gb display."""
598
gb = bytes / 1024 / 1024 / 1024
600
mb = bytes / 1024 / 1024
602
return (bytes / 1024, 'KB')
606
def update_quota_display(self, used, total):
607
"""Update the quota display."""
608
percent = (float(used) / float(total)) * 100
609
real_used, real_type = self._format_for_gb_display(used)
610
self.usage_label.set_text(
611
_("%(used)0.1f %(type)s Used (%(percent)0.1f%%)") % {
614
'percent' : percent })
615
self.usage_graph.set_fraction(percent / 100)
618
self.overquota_img.set_from_icon_name('dialog-warning',
619
gtk.ICON_SIZE_DIALOG)
620
self.overquota.show_all()
621
label = _("You are using all of your Ubuntu One storage.")
622
desc = _("Synchronization is now disabled. Remove files from"
623
" synchronization or upgrade your subscription.")
625
self.overquota_img.set_from_icon_name('dialog-information',
626
gtk.ICON_SIZE_DIALOG)
627
self.overquota.show_all()
628
label = _("You are near your Ubuntu One storage limit.")
629
desc = _("Automatic synchronization will stop when you reach"
630
" your storage limit. Please consider upgrading to"
631
" avoid a service interruption.")
633
self.overquota_img.clear()
635
self.overquota.hide_all()
636
self.overquota_label.set_markup("%s\n<small>%s</small>" % (label, desc))
638
def got_quota_info(self, quota):
639
"""Handle the result from the quota REST call."""
641
used = quota.get('used', 0)
642
total = quota.get('total', 2)
643
self.update_quota_display(used, total)
645
def request_quota_info(self):
646
"""Request new quota info from server, and update display."""
647
make_rest_request(url='https://one.ubuntu.com/api/quota/',
648
callback=self.got_quota_info)
650
def got_account_info(self, user):
651
"""Handle the result from the account REST call."""
653
self.name_label.set_text(user.get('nickname', _("Unknown")))
654
self.mail_label.set_text(user.get('email', _("Unknown")))
655
if "current_plan" in user:
656
desc = user["current_plan"]
657
elif "subscription" in user \
658
and "description" in user["subscription"]:
659
desc = user["subscription"]["description"]
662
self.plan_label.set_label(desc)
664
def request_account_info(self):
665
"""Request account info from server, and update display."""
666
make_rest_request(url='https://one.ubuntu.com/api/account/',
667
callback=self.got_account_info)
669
def __bw_inst_cb(self, button):
670
"""Pops off the bindwood installer to a thread."""
671
Thread(target=self.__install_bindwood).start()
673
def __install_bindwood(self):
674
"""Runs the tool to intall bindwood and updates the UI."""
675
installed = subprocess.call(BW_INST_ARGS)
676
gtk.gdk.threads_enter()
678
self.bw_inst_box.hide()
680
self.bw_inst_box.show()
681
logger.error("There was an error installing bindwood.")
682
gtk.gdk.threads_leave()
684
def __check_for_bindwood(self):
686
Method run in a thread to check that bindwood exists, and
687
update the interface appropriately.
690
p = subprocess.Popen(BW_CHCK_ARGS, bufsize=4096,
691
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
692
stdin=subprocess.PIPE, env=os.environ)
695
for line in p.stdout.readlines():
696
if line.startswith('un'):
701
gtk.gdk.threads_enter()
703
self.bw_inst_box.hide()
705
self.bw_inst_btn.connect("clicked",
707
self.bw_inst_box.show()
708
gtk.gdk.threads_leave()
710
def connect_desktopcouch_exclusion(self):
711
"""Hook up to desktopcouch enablement API, or disable the UI."""
712
# Hook up to desktopcouch, or die trying
715
raise ValueError('Replication service not available.')
717
# Check bindwood availability
718
Thread(target=self.__check_for_bindwood).start()
720
# Get the exclusion settings
721
self.dcouch = dcouch.ReplicationExclusion()
722
exclusions = self.dcouch.all_exclusions()
723
for check in [self.bookmarks_check,
726
if check.get_data('dbname') in exclusions:
727
check.set_active(False)
729
check.set_active(True)
730
except ValueError, e:
731
# Replication is not avaialble, so disable the UI
732
for check in [self.bookmarks_check,
735
check.set_sensitive(False)
738
def toggle_db_sync(self, dbname, disable=False):
740
Toggle whether a db in desktopcouch is synchronized to the
741
Ubuntu One couchdb server.
745
self.dcouch.exclude(dbname)
747
self.dcouch.replicate(dbname)
749
logger.error("Database enablement is unavailable.")
751
def __db_check_toggled(self, checkbutton):
752
"""Handle toggling a desktopcouch service toggling."""
753
dbname = checkbutton.get_data('dbname')
754
self.toggle_db_sync(dbname, not checkbutton.get_active())
756
def files_check_toggled(self, checkbutton):
757
"""Handle toggling the files service."""
758
enabled = checkbutton.get_active()
759
self.sdtool.enable_files_sync(enabled).addCallbacks(
760
lambda _: dbus_async,
764
self.sdtool.connect()
765
self.devices.list_devices()
767
self.files_check.set_active(False)
768
self.files_check.set_sensitive(False)
769
self.music_check.set_sensitive(False)
770
self.__sd_error(error)
773
self.sdtool.start().addCallbacks(sd_enabled,
776
self.music_check.set_sensitive(True)
777
if self.music_check.get_active():
778
self.music_check_toggled(self.music_check)
780
self.sdtool.quit().addCallback(
781
lambda _: self.devices.list_devices())
782
self.music_check.set_sensitive(False)
784
def music_check_toggled(self, checkbutton):
785
"""Handle toggling the music download service."""
786
if not self.files_check.get_active() or not self.ums_id:
787
checkbutton.set_sensitive(False)
788
checkbutton.set_active(False)
790
enabled = checkbutton.get_active()
791
def got_error(error):
792
checkbutton.set_sensitive(False)
793
checkbutton.set_active(not enabled)
794
self.__sd_error(error)
797
d = self.sdtool.subscribe_folder(self.ums_id)
799
d = self.sdtool.unsubscribe_folder(self.ums_id)
800
d.addErrback(got_error)
802
def get_syncdaemon_sync_config(self):
803
"""Update the files/music sync config from syncdaemon."""
804
def got_ms_err(error):
805
self.music_check.set_sensitive(False)
806
self.__sd_error(error)
810
self.music_check.set_active(False)
811
self.music_check.set_sensitive(False)
813
self.ums_id = info['volume_id']
814
self.music_check.set_sensitive(True)
815
if info['subscribed'] == 'True':
816
self.music_check.set_active(True)
818
self.music_check.set_active(False)
819
if os.path.exists(U1MSPATH):
820
self.sdtool.get_folder_info(
821
U1MSPATH).addCallbacks(got_info, got_ms_err)
822
self.files_check.set_active(self.sdtool.is_files_sync_enabled())
824
def connect_file_sync_callbacks(self):
825
"""Connect the file sync checkbox callbacks."""
826
self.files_check.connect('toggled', self.files_check_toggled)
827
self.music_check.connect('toggled', self.music_check_toggled)
829
def __construct(self):
830
"""Construct the dialog's layout."""
831
area = self.get_content_area()
833
vbox = gtk.VBox(spacing=12)
834
vbox.set_border_width(12)
838
# Usage text/progress bar
839
rbox = gtk.VBox(spacing=12)
840
vbox.pack_start(rbox, False, False)
843
hbox = gtk.HBox(spacing=6)
844
rbox.pack_start(hbox, False, False)
847
self.usage_graph = gtk.ProgressBar()
848
hbox.pack_start(self.usage_graph, False, False)
849
self.usage_graph.show()
851
self.usage_label = gtk.Label(_("Unknown"))
852
self.usage_label.set_alignment(0.0, 0.5)
853
hbox.pack_start(self.usage_label, False, False)
854
self.usage_label.show()
856
self.status_label = gtk.Label(_("Unknown"))
857
self.status_label.set_alignment(0.0, 0.5)
858
rbox.pack_start(self.status_label, False, False)
859
self.status_label.show()
862
self.notebook = gtk.Notebook()
863
vbox.add(self.notebook)
867
account = gtk.VBox(spacing=12)
868
account.set_border_width(6)
869
self.notebook.append_page(account)
870
self.notebook.set_tab_label_text(account, _("Account"))
873
# overquota notice in account tab
874
self.overquota = gtk.HBox(spacing=6)
875
self.overquota.set_border_width(6)
876
account.pack_start(self.overquota, False, False)
878
self.overquota_img = gtk.Image()
879
self.overquota.pack_start(self.overquota_img, False, False)
880
self.overquota_img.show()
882
self.overquota_label = label = gtk.Label("\n\n")
883
label.set_line_wrap(True)
884
label.set_alignment(0.0, 0.5)
885
self.overquota.pack_start(label, False, False)
886
self.overquota_label.show()
889
# User info in account tab
890
table = gtk.Table(rows=6, columns=2)
891
table.set_row_spacings(6)
892
table.set_col_spacings(6)
893
account.pack_start(table, False, False)
896
label = gtk.Label(_("_Name:"))
897
label.set_use_underline(True)
898
label.set_alignment(0.0, 0.5)
899
table.attach(label, 0, 1, 0, 1)
902
self.name_label = gtk.Label(_("Unknown"))
903
self.name_label.set_use_underline(True)
904
self.name_label.set_alignment(0.0, 0.5)
905
table.attach(self.name_label, 1, 2, 0, 1)
906
self.name_label.show()
908
label = gtk.Label(_("_E-mail:"))
909
label.set_use_underline(True)
910
label.set_alignment(0.0, 0.5)
911
table.attach(label, 0, 1, 1, 2)
914
self.mail_label = gtk.Label(_("Unknown"))
915
self.mail_label.set_use_underline(True)
916
self.mail_label.set_alignment(0.0, 0.5)
917
table.attach(self.mail_label, 1, 2, 1, 2)
918
self.mail_label.show()
920
label = gtk.Label(_("Current plan:"))
921
label.set_alignment(0.0, 0.5)
922
table.attach(label, 0, 1, 2, 3)
925
self.plan_label = gtk.Label(_("Unknown"))
926
self.plan_label.set_alignment(0.0, 0.5)
927
table.attach(self.plan_label, 1, 2, 2, 3)
928
self.plan_label.show()
930
for n, (url, label) in enumerate([
931
("http://one.ubuntu.com/support", _("Support options")),
932
("http://one.ubuntu.com/account", _("Manage account")),
933
("http://one.ubuntu.com/upgrade",
934
_("Upgrade your subscription")),
936
link = gtk.LinkButton(url, label)
937
link.set_relief(gtk.RELIEF_NONE)
938
link.set_alignment(0.0, 0.5)
939
table.attach(link, 0, 2, 5-n, 6-n)
943
sw = gtk.ScrolledWindow()
944
sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
945
self.notebook.append_page(sw)
946
self.notebook.set_tab_label_text(sw, _("Devices"))
948
self.devices = DevicesWidget(self.__bus)
949
sw.add_with_viewport(self.devices)
950
self.devices.list_devices()
951
self.devices.show_all()
954
services = gtk.VBox(spacing=12)
955
services.set_border_width(6)
956
self.notebook.append_page(services)
957
self.notebook.set_tab_label_text(services, _("Services"))
962
_("Ubuntu One sync options\n"
963
"<small>Choose services to synchronize"
964
" with this computer</small>"))
965
label.set_alignment(0., .5)
966
label.set_padding(12, 6)
967
label.set_line_wrap(True)
968
services.pack_start(label, False, False)
971
self.bookmarks_check = gtk.CheckButton(_("_Bookmarks"))
972
self.bookmarks_check.set_data('dbname', 'bookmarks')
973
self.bookmarks_check.connect('toggled', self.__db_check_toggled)
974
services.pack_start(self.bookmarks_check, False, False)
975
self.bookmarks_check.show()
977
# This box is shown in the event bindwood is not installed
978
self.bw_inst_box = gtk.HBox(spacing=12)
979
services.pack_start(self.bw_inst_box, False, False)
981
label = gtk.Label("")
982
self.bw_inst_box.pack_start(label, False, False)
985
label = gtk.Label("<i>%s</i>" % _("Firefox extension not installed."))
986
label.set_use_markup(True)
987
label.set_alignment(0.0, 0.5)
988
self.bw_inst_box.pack_start(label, False, False)
991
self.bw_inst_btn = gtk.Button(_("_Install"))
992
self.bw_inst_box.pack_start(self.bw_inst_btn, False, False)
993
self.bw_inst_btn.show()
995
self.gwib_check = gtk.CheckButton(_("Broadcast messages _archive"))
996
self.gwib_check.set_data('dbname', 'gwibber_messages')
997
self.gwib_check.connect('toggled', self.__db_check_toggled)
998
services.pack_start(self.gwib_check, False, False)
999
self.gwib_check.show()
1001
self.abook_check = gtk.CheckButton(_("C_ontacts"))
1002
self.abook_check.set_data('dbname', 'contacts')
1003
self.abook_check.connect('toggled', self.__db_check_toggled)
1004
services.pack_start(self.abook_check, False, False)
1005
self.abook_check.show()
1007
fbox = gtk.VBox(spacing=6)
1008
services.pack_start(fbox, False, False)
1011
self.files_check = gtk.CheckButton(_("_Files"))
1012
self.files_check.set_active(False)
1013
fbox.pack_start(self.files_check, False, False)
1014
self.files_check.show()
1016
alignment = gtk.Alignment(0.0, 0.5)
1017
alignment.set_padding(0, 0, 12, 0)
1018
fbox.pack_start(alignment, False, False)
1021
self.music_check = gtk.CheckButton(_("Ubuntu One _Music Store"
1023
self.music_check.set_active(True)
1024
self.music_check.set_sensitive(False)
1025
alignment.add(self.music_check)
1026
self.music_check.show()
1029
class UbuntuoneLoginHandler(dbus.service.Object):
1030
"""Class to handle registration/login, in case we aren't already."""
1032
def __init__(self, dialog, *args, **kw):
1033
self.bus = dbus.SessionBus()
1035
self.dialog = dialog
1039
bus_name = dbus.service.BusName(PREFS_BUS_NAME, bus=self.bus)
1040
dbus.service.Object.__init__(self, bus_name=bus_name,
1041
object_path=self.path)
1044
@dbus.service.method(PREFS_BUS_NAME, in_signature='', out_signature='')
1046
"""Raise the dialog window."""
1047
self.dialog.connect_desktopcouch_exclusion()
1048
self.dialog.get_syncdaemon_sync_config()
1049
self.dialog.connect_file_sync_callbacks()
1050
self.dialog.request_quota_info()
1051
self.dialog.request_account_info()
1052
self.dialog.devices.get_devices()
1053
# watch it! jaunty has no 'get_visible' method
1054
if self.dialog.get_property('visible'):
1055
self.dialog.present_with_time(int(time.time()))
1057
def got_newcredentials(self, app_name, credentials):
1058
"""Show our dialog, since we can do stuff now."""
1059
global oauth_consumer
1062
if app_name == clientdefs.APP_NAME:
1063
oauth_consumer = oauth.OAuthConsumer(credentials['consumer_key'],
1064
credentials['consumer_secret'])
1065
oauth_token = oauth.OAuthToken(credentials['token'],
1066
credentials['token_secret'])
1068
logger.info("Got credentials for %s", app_name)
1070
def got_credentialserror(self, app_name, error_dict):
1071
"""Got an error during authentication."""
1072
message = error_dict.get('error_msg', '')
1073
detailed_error = error_dict.get('detailed_error', '')
1074
if app_name == clientdefs.APP_NAME:
1075
logger.error("Credentials error for %s: %s - %s" %
1076
(app_name, message, detailed_error))
1078
def got_authdenied(self, app_name):
1079
"""User denied access."""
1080
if app_name == clientdefs.APP_NAME:
1081
logger.error("Authorization was denied for %s" % app_name)
1083
def got_dbus_error(self, error):
1084
"""Got a DBusError."""
1087
def register_signal_handlers(self):
1088
"""Register the dbus signal handlers."""
1089
self.bus.add_signal_receiver(
1090
handler_function=self.got_newcredentials,
1091
signal_name='CredentialsFound',
1092
dbus_interface=DBUS_CREDENTIALS_IFACE)
1093
self.bus.add_signal_receiver(
1094
handler_function=self.got_credentialserror,
1095
signal_name='CredentialsError',
1096
dbus_interface=DBUS_CREDENTIALS_IFACE)
1097
self.bus.add_signal_receiver(
1098
handler_function=self.got_authdenied,
1099
signal_name='AuthorizationDenied',
1100
dbus_interface=DBUS_CREDENTIALS_IFACE)
1103
if __name__ == "__main__":
1104
gettext.bindtextdomain(clientdefs.GETTEXT_PACKAGE, clientdefs.LOCALEDIR)
1105
gettext.textdomain(clientdefs.GETTEXT_PACKAGE)
1107
gobject.threads_init()
1108
gtk.gdk.threads_init()
1110
gtk.rc_parse_string(RCSTYLE)
1111
gobject.set_application_name("ubuntuone-preferences")
1113
bus = dbus.SessionBus()
1114
result = bus.request_name(PREFS_BUS_NAME,
1115
dbus.bus.NAME_FLAG_DO_NOT_QUEUE)
1116
if result == dbus.bus.REQUEST_NAME_REPLY_EXISTS:
1118
client = bus.get_object(PREFS_BUS_NAME, '/',
1119
follow_name_owner_changes=True)
1120
iface= dbus.Interface(client, PREFS_BUS_NAME)
1122
except DBusException, e:
1128
gtk.gdk.threads_enter()
1130
prefs_dialog = UbuntuOneDialog()
1133
login = UbuntuoneLoginHandler(dialog=prefs_dialog)
1134
login.register_signal_handlers()
1135
gobject.timeout_add_seconds(1, do_login_request,
1136
bus, login.got_dbus_error)
1138
gtk.gdk.threads_leave()
1139
except KeyboardInterrupt: