25
27
from twisted.internet import reactor, defer
29
from ubuntuone.clientdefs import GETTEXT_PACKAGE
27
30
from ubuntuone.logger import basic_formatter, logging
31
from ubuntuone.platform import session
28
32
from ubuntuone.platform.notification import Notification
29
33
from ubuntuone.platform.messaging import Messaging, open_volumes
34
from ubuntuone.platform.unity import UbuntuOneLauncher
36
Q_ = lambda string: gettext.dgettext(GETTEXT_PACKAGE, string)
31
37
LOG_LEVEL = logging.DEBUG
32
38
logger = logging.getLogger('ubuntuone.status')
33
39
logger.setLevel(LOG_LEVEL)
38
44
debug_handler.setLevel(LOG_LEVEL)
39
45
logger.addHandler(debug_handler)
41
UBUNTUONE_TITLE = "Ubuntu One"
42
NEW_UDFS_SENDER = "New Cloud Folder(s) Available"
43
FINAL_COMPLETED = "File synchronization completed."
44
FINAL_UPLOADED = "%d files were uploaded to your cloud."
45
FINAL_DOWNLOADED = "%d files were downloaded to your computer."
46
FILES_UPLOADING = "%d files are uploading to your cloud."
47
FILES_DOWNLOADING = "%d files are downloading to your computer."
48
PROGRESS_UPLOADED = "Uploaded %d/%d files."
49
PROGRESS_DOWNLOADED = "Downloaded %d/%d files."
50
PROGRESS_COMPLETED = "%d%% completed."
47
UBUNTUONE_TITLE = Q_("Ubuntu One")
48
NEW_UDFS_SENDER = Q_("New cloud folder(s) available")
49
FINAL_COMPLETED = Q_("File synchronization completed.")
50
FINAL_UPLOADED = Q_("%(total_uploaded_files)d file(s) were uploaded to " +
51
"your personal cloud.")
52
FINAL_DOWNLOADED = Q_("%(total_downloaded_files)d file(s) were downloaded " +
54
FILES_UPLOADING = Q_("%(total_uploading_files)d file(s) are uploading to " +
55
"your personal cloud.")
56
FILES_DOWNLOADING = Q_("%(total_downloading_files)d file(s) are downloading " +
58
PROGRESS_COMPLETED = Q_("%(percentage_completed)d%% completed.")
59
FILE_SYNC_IN_PROGRESS = Q_("File synchronization in progress")
62
class ToggleableNotification(object):
63
"""A controller for notifications that can be turned off."""
65
def __init__(self, notification_switch):
66
"""Initialize this instance."""
67
self.notification_switch = notification_switch
68
self.notification = Notification()
70
def send_notification(self, *args):
71
"""Passthru the notification."""
72
if self.notification_switch.enabled:
73
return self.notification.send_notification(*args)
75
def update_notification(self, notification_id, *args):
76
"""Passthru the update."""
77
if notification_id is not None and self.notification_switch.enabled:
78
return self.notification.update_notification(notification_id,
82
class NotificationSwitch(object):
83
"""A switch that turns notifications on and off."""
87
def build_notification(self):
88
"""Return a new notification instance."""
89
return ToggleableNotification(self)
91
def enable_notifications(self):
92
"""Turn the switch on."""
95
def disable_notifications(self):
96
"""Turn the switch off."""
53
100
class StatusEvent(object):
59
106
DO_NOT_INSTANCE = "Do not instance this class, only children."""
61
def __init__(self, *args):
108
def __init__(self, **kwargs):
62
109
"""Initialize this instance."""
63
110
assert type(self) != StatusEvent, self.DO_NOT_INSTANCE
67
114
"""A message if this is the only event of this type."""
70
117
def many(self, events):
71
118
"""A message if there are many events of this type."""
72
return self.MESSAGE_MANY % len(events)
119
format_args = {"event_count": len(events)}
120
return self.MESSAGE_MANY % format_args
75
123
class FilePublishingStatus(StatusEvent):
76
124
"""Files that are made public with a url."""
78
MESSAGE_ONE = "A file was just made public at %s"
79
MESSAGE_MANY = "%d files were just made public"
126
MESSAGE_ONE = Q_("A file was just made public at %(new_public_url)s")
127
MESSAGE_MANY = Q_("%(event_count)d files were just made public")
83
131
"""Show the url if only one event of this type."""
84
return self.MESSAGE_ONE % self.args
132
return self.MESSAGE_ONE % self.kwargs
87
135
class FileUnpublishingStatus(StatusEvent):
88
136
"""Files that have stopped being published."""
90
MESSAGE_ONE = "A file is no longer published"
91
MESSAGE_MANY = "%d files are no longer published"
138
MESSAGE_ONE = Q_("A file is no longer published")
139
MESSAGE_MANY = Q_("%(event_count)d files are no longer published")
95
143
class FolderAvailableStatus(StatusEvent):
96
144
"""Folders available for subscription."""
98
MESSAGE_MANY = "Found %d new cloud folders."""
146
MESSAGE_MANY = Q_("Found %(event_count)d new cloud folders.")
102
150
class ShareAvailableStatus(FolderAvailableStatus):
103
151
"""A Share is available for subscription."""
105
MESSAGE_ONE = "New cloud folder available: <%s> shared by <%s>"""
153
MESSAGE_ONE = Q_("New cloud folder available: '%(folder_name)s' "
154
"shared by %(other_user_name)s")
108
157
"""Show the folder information."""
109
volume = self.args[0]
110
params = (volume.name, volume.other_visible_name)
111
return self.MESSAGE_ONE % params
158
volume = self.kwargs["share"]
160
"folder_name": volume.name,
161
"other_user_name": volume.other_visible_name,
163
return self.MESSAGE_ONE % format_args
114
166
class UDFAvailableStatus(FolderAvailableStatus):
115
167
"""An UDF is available for subscription."""
117
MESSAGE_ONE = "New cloud folder available: %s"""
169
MESSAGE_ONE = Q_("New cloud folder available: '%(folder_name)s'")
120
172
"""Show the folder information."""
121
volume = self.args[0]
122
params = (volume.suggested_path,)
123
return self.MESSAGE_ONE % params
173
volume = self.kwargs["udf"]
174
format_args = {"folder_name": volume.suggested_path}
175
return self.MESSAGE_ONE % format_args
126
178
class ConnectionStatusEvent(StatusEvent):
137
189
class ConnectionLostStatus(ConnectionStatusEvent):
138
190
"""The connection to the server was lost."""
140
MESSAGE_ONE = "The connection to the server was lost."""
192
MESSAGE_ONE = Q_("The connection to the server was lost.")
143
195
class ConnectionMadeStatus(ConnectionStatusEvent):
144
196
"""The connection to the server was made."""
146
MESSAGE_ONE = "The connection to the server was restored."""
198
MESSAGE_ONE = Q_("The connection to the server was restored.")
149
201
class Timer(defer.Deferred):
398
450
"""Update a progressbar no more than 10 times a second."""
402
454
updates_delay = 0.1
456
inhibitor_defer = None
405
458
def __init__(self, clock=reactor):
406
459
"""Initialize this instance."""
407
460
self.clock = clock
461
self.launcher = UbuntuOneLauncher()
409
463
def cleanup(self):
410
464
"""Cleanup this instance."""
412
466
self.timer.cleanup()
415
"""Enable the pulse on the progressbar."""
416
self.pulsating = True
419
"""Disable the pulse on the progressbar."""
420
self.pulsating = False
469
def hide_emblem(self):
470
"""Hide the emblem on the launcher icon."""
471
self.emblem_visible = False
472
self.launcher.hide_emblem()
474
def show_warning_emblem(self):
475
"""Show the warning emblem on the launcher icon."""
476
self.emblem_visible = True
477
self.launcher.show_warning_emblem()
422
479
def _timeout(self, result):
423
480
"""The aggregating timer has expired, so update the UI."""
424
481
self.timer = None
482
self.launcher.set_progress(self.progress)
483
logger.debug("progressbar updated: %f", self.progress)
426
485
def progress_made(self, steps_done, steps_total):
427
486
"""Steps amount changed. Set up a timer if one not ticking."""
428
487
assert steps_total > 0
429
self.percentage = 100.0 * float(steps_done) / float(steps_total)
488
self.progress = float(steps_done) / float(steps_total)
430
489
if not self.visible:
431
490
self.visible = True
491
self.launcher.show_progressbar()
492
logger.debug("progressbar shown")
493
if self.inhibitor_defer is None:
494
self.inhibitor_defer = session.inhibit_logout_suspend(
495
FILE_SYNC_IN_PROGRESS)
432
496
if not self.timer:
433
497
self.timer = Timer(self.updates_delay, clock=self.clock)
434
498
self.timer.addCallback(self._timeout)
437
501
"""All has completed."""
439
503
self.visible = False
504
self.launcher.hide_progressbar()
505
logger.debug("progressbar hidden")
506
if self.inhibitor_defer is not None:
508
def inhibitor_callback(inhibitor):
509
"""The inhibitor was found, so cancel it."""
510
self.inhibitor_defer = None
511
return inhibitor.cancel()
513
self.inhibitor_defer.addCallback(inhibitor_callback)
442
516
class FinalStatusBubble(object):
475
549
def __init__(self, clock=reactor):
476
550
"""Initialize this instance."""
477
551
self.clock = clock
552
self.notification_switch = NotificationSwitch()
479
554
self.progress_bar = ProgressBar(clock=self.clock)
556
def build_notification(self):
557
"""Create a new toggleable notification object."""
558
return self.notification_switch.build_notification()
482
561
"""Reset all counters and notifications."""
483
562
self.total_counter = 0
509
588
if len(self.files_uploading) > 0:
510
text = FILES_UPLOADING % len(self.files_uploading)
589
format_args = {"total_uploading_files": len(self.files_uploading)}
590
text = FILES_UPLOADING % format_args
511
591
lines.append(text)
513
593
if len(self.files_downloading) > 0:
514
text = FILES_DOWNLOADING % len(self.files_downloading)
594
num_files_downloading = len(self.files_downloading)
595
format_args = {"total_downloading_files": num_files_downloading}
596
text = FILES_DOWNLOADING % format_args
515
597
lines.append(text)
517
599
return "\n".join(lines)
521
603
assert self.total_counter > 0
523
605
if self.upload_total:
524
parts.append(PROGRESS_UPLOADED % (self.upload_done,
606
format_args = {"total_uploading_files": self.upload_total}
607
parts.append(FILES_UPLOADING % format_args)
526
608
if self.download_total:
527
parts.append(PROGRESS_DOWNLOADED % (self.download_done,
528
self.download_total))
609
format_args = {"total_downloading_files": self.download_total}
610
parts.append(FILES_DOWNLOADING % format_args)
529
611
progress_percentage = 100.0 * self.done_counter / self.total_counter
530
parts.append(PROGRESS_COMPLETED % int(progress_percentage))
531
return " ".join(parts)
612
format_args = {"percentage_completed": int(progress_percentage)}
613
parts.append(PROGRESS_COMPLETED % format_args)
614
return "\n".join(parts)
533
616
def get_final_status_message(self):
534
617
"""Get some lines describing all we did."""
536
619
parts.append(FINAL_COMPLETED)
537
620
if self.upload_done:
538
parts.append(FINAL_UPLOADED % self.upload_done)
621
format_args = {'total_uploaded_files': self.upload_done}
622
parts.append(FINAL_UPLOADED % format_args)
539
623
if self.download_done:
540
parts.append(FINAL_DOWNLOADED % self.download_done)
624
format_args = {'total_downloaded_files': self.download_done}
625
parts.append(FINAL_DOWNLOADED % format_args)
541
626
return "\n".join(parts)
543
628
def restart_progress_bubble(self):
547
632
def queue_done(self):
548
633
"""Show final bubble and reset counters."""
549
self.final_status_bubble.show()
634
if self.upload_done + self.download_done > 0:
635
self.final_status_bubble.show()
550
636
self.progress_bar.completed()
553
def no_callback(self, status_events):
554
"""Pushed a bunch of events by the buffer."""
558
if self.show_progress:
559
lines.append(self.get_progress_message())
561
for weight, statuses in group_statuses(status_events):
562
statuses = list(statuses)
563
count = len(statuses)
564
first_status = statuses[0]
566
lines.append(first_status.one())
568
lines.append(first_status.many(count))
570
text = "\n".join(lines)
571
self.notification.send_notification(UBUNTUONE_TITLE, text)
572
logger.debug("notification shown: %s", text)
573
self.show_progress = False
575
639
def misc_command_queued(self, command):
576
640
"""A miscellaneous command was queued."""
577
641
self.total_counter += 1
618
682
self.upload_done += 1
619
683
self.misc_command_unqueued(command)
685
def connection_lost(self):
686
"""The connection to the server was lost."""
687
self.progress_bar.show_warning_emblem()
689
def connection_made(self):
690
"""The connection to the server was made."""
691
self.progress_bar.hide_emblem()
622
694
class StatusFrontend(object):
623
695
"""Frontend for the status aggregator, used by the StatusListener."""
625
697
def __init__(self, clock=reactor):
626
698
"""Initialize this instance."""
627
699
self.aggregator = StatusAggregator(clock=clock)
628
self.notification = Notification()
700
self.notification = self.aggregator.build_notification()
629
701
self.messaging = Messaging()
630
702
self.udf_message = None
632
704
def file_published(self, public_url):
633
705
"""A file was published."""
706
status_event = FilePublishingStatus(new_public_url=public_url)
634
707
self.notification.send_notification(
635
UBUNTUONE_TITLE, FilePublishingStatus(public_url).one())
708
UBUNTUONE_TITLE, status_event.one())
637
710
def file_unpublished(self, public_url): # pylint: disable=W0613
638
711
"""A file was unpublished."""
671
744
"""A new share is available for subscription."""
672
745
self.messaging.show_message(share.other_visible_name)
673
746
self.notification.send_notification(
674
UBUNTUONE_TITLE, ShareAvailableStatus(share).one())
747
UBUNTUONE_TITLE, ShareAvailableStatus(share=share).one())
676
749
def new_udf_available(self, udf):
677
750
"""A new udf is available for subscription."""
682
755
NEW_UDFS_SENDER, callback=self._reset_udf_message_callback,
684
757
self.notification.send_notification(
685
UBUNTUONE_TITLE, UDFAvailableStatus(udf).one())
758
UBUNTUONE_TITLE, UDFAvailableStatus(udf=udf).one())
687
760
def _reset_udf_message_callback(self, indicator, message_time=None):
688
761
"""A callback wrapper that resets the udf_message to None."""
694
767
logger.debug("server connection lost")
695
768
self.notification.send_notification(
696
769
UBUNTUONE_TITLE, ConnectionLostStatus().one())
770
self.aggregator.connection_lost()
698
772
def server_connection_made(self):
699
773
"""The client made the connection to the server."""
700
774
logger.debug("server connection made")
701
775
self.notification.send_notification(
702
776
UBUNTUONE_TITLE, ConnectionMadeStatus().one())
777
self.aggregator.connection_made()
779
def set_show_all_notifications(self, value):
780
"""Set the flag to show all notifications."""
782
self.aggregator.notification_switch.enable_notifications()
784
self.aggregator.notification_switch.disable_notifications()