~ubuntu-branches/ubuntu/oneiric/ubuntuone-client/oneiric

« back to all changes in this revision

Viewing changes to ubuntuone/status/aggregator.py

  • Committer: Bazaar Package Importer
  • Author(s): Rodney Dawes
  • Date: 2011-02-23 18:34:09 UTC
  • mfrom: (1.1.45 upstream)
  • Revision ID: james.westby@ubuntu.com-20110223183409-535o7yo165wbjmca
Tags: 1.5.5-0ubuntu1
* New upstream release.
  - Subscribing to a RO share will not download content (LP: #712528)
  - Can't synchronize "~/Ubuntu One Music" (LP: #714976)
  - Syncdaemon needs to show progress in Unity launcher (LP: #702116)
  - Notifications say "your cloud" (LP: #715887)
  - No longer requires python-libproxy
  - Recommend unity and indicator libs by default

Show diffs side-by-side

added added

removed removed

Lines of Context:
22
22
import os
23
23
import sys
24
24
 
 
25
import gettext
 
26
 
25
27
from twisted.internet import reactor, defer
26
28
 
 
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
30
35
 
 
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)
40
46
 
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 " +
 
53
                      "to your computer.")
 
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 " +
 
57
                       "to your computer.")
 
58
PROGRESS_COMPLETED = Q_("%(percentage_completed)d%% completed.")
 
59
FILE_SYNC_IN_PROGRESS = Q_("File synchronization in progress")
 
60
 
 
61
 
 
62
class ToggleableNotification(object):
 
63
    """A controller for notifications that can be turned off."""
 
64
 
 
65
    def __init__(self, notification_switch):
 
66
        """Initialize this instance."""
 
67
        self.notification_switch = notification_switch
 
68
        self.notification = Notification()
 
69
 
 
70
    def send_notification(self, *args):
 
71
        """Passthru the notification."""
 
72
        if self.notification_switch.enabled:
 
73
            return self.notification.send_notification(*args)
 
74
 
 
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,
 
79
                                                         *args)
 
80
 
 
81
 
 
82
class NotificationSwitch(object):
 
83
    """A switch that turns notifications on and off."""
 
84
 
 
85
    enabled = True
 
86
 
 
87
    def build_notification(self):
 
88
        """Return a new notification instance."""
 
89
        return ToggleableNotification(self)
 
90
 
 
91
    def enable_notifications(self):
 
92
        """Turn the switch on."""
 
93
        self.enabled = True
 
94
 
 
95
    def disable_notifications(self):
 
96
        """Turn the switch off."""
 
97
        self.enabled = False
51
98
 
52
99
 
53
100
class StatusEvent(object):
58
105
    WEIGHT = 99
59
106
    DO_NOT_INSTANCE = "Do not instance this class, only children."""
60
107
 
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
64
 
        self.args = args
 
111
        self.kwargs = kwargs
65
112
 
66
113
    def one(self):
67
114
        """A message if this is the only event of this type."""
69
116
 
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
73
121
 
74
122
 
75
123
class FilePublishingStatus(StatusEvent):
76
124
    """Files that are made public with a url."""
77
125
 
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")
80
128
    WEIGHT = 50
81
129
 
82
130
    def one(self):
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
85
133
 
86
134
 
87
135
class FileUnpublishingStatus(StatusEvent):
88
136
    """Files that have stopped being published."""
89
137
 
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")
92
140
    WEIGHT = 51
93
141
 
94
142
 
95
143
class FolderAvailableStatus(StatusEvent):
96
144
    """Folders available for subscription."""
97
145
 
98
 
    MESSAGE_MANY = "Found %d new cloud folders."""
 
146
    MESSAGE_MANY = Q_("Found %(event_count)d new cloud folders.")
99
147
    WEIGHT = 60
100
148
 
101
149
 
102
150
class ShareAvailableStatus(FolderAvailableStatus):
103
151
    """A Share is available for subscription."""
104
152
 
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")
106
155
 
107
156
    def one(self):
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"]
 
159
        format_args = {
 
160
            "folder_name": volume.name,
 
161
            "other_user_name": volume.other_visible_name,
 
162
        }
 
163
        return self.MESSAGE_ONE % format_args
112
164
 
113
165
 
114
166
class UDFAvailableStatus(FolderAvailableStatus):
115
167
    """An UDF is available for subscription."""
116
168
 
117
 
    MESSAGE_ONE = "New cloud folder available: %s"""
 
169
    MESSAGE_ONE = Q_("New cloud folder available: '%(folder_name)s'")
118
170
 
119
171
    def one(self):
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
124
176
 
125
177
 
126
178
class ConnectionStatusEvent(StatusEvent):
137
189
class ConnectionLostStatus(ConnectionStatusEvent):
138
190
    """The connection to the server was lost."""
139
191
 
140
 
    MESSAGE_ONE = "The connection to the server was lost."""
 
192
    MESSAGE_ONE = Q_("The connection to the server was lost.")
141
193
 
142
194
 
143
195
class ConnectionMadeStatus(ConnectionStatusEvent):
144
196
    """The connection to the server was made."""
145
197
 
146
 
    MESSAGE_ONE = "The connection to the server was restored."""
 
198
    MESSAGE_ONE = Q_("The connection to the server was restored.")
147
199
 
148
200
 
149
201
class Timer(defer.Deferred):
328
380
 
329
381
    def _start(self):
330
382
        """The first file was found, so start gathering."""
331
 
        self.notification = Notification()
 
383
        self.notification = self.status_aggregator.build_notification()
332
384
        self._change_state(FileDiscoveryGatheringState)
333
385
 
334
386
    def _popup(self):
381
433
 
382
434
    def _timeout(self, result):
383
435
        """Show the bubble."""
384
 
        self.notification = Notification()
 
436
        self.notification = self.status_aggregator.build_notification()
385
437
        text = self.status_aggregator.get_progress_message()
386
438
        self.notification.send_notification(UBUNTUONE_TITLE, text)
387
439
        self.restart()
398
450
    """Update a progressbar no more than 10 times a second."""
399
451
    pulsating = True
400
452
    visible = False
401
 
    percentage = 0.0
 
453
    progress = 0.0
402
454
    updates_delay = 0.1
403
455
    timer = None
 
456
    inhibitor_defer = None
404
457
 
405
458
    def __init__(self, clock=reactor):
406
459
        """Initialize this instance."""
407
460
        self.clock = clock
 
461
        self.launcher = UbuntuOneLauncher()
408
462
 
409
463
    def cleanup(self):
410
464
        """Cleanup this instance."""
411
465
        if self.timer:
412
466
            self.timer.cleanup()
413
 
 
414
 
    def enable(self):
415
 
        """Enable the pulse on the progressbar."""
416
 
        self.pulsating = True
417
 
 
418
 
    def disable(self):
419
 
        """Disable the pulse on the progressbar."""
420
 
        self.pulsating = False
 
467
            self.timer = None
 
468
 
 
469
    def hide_emblem(self):
 
470
        """Hide the emblem on the launcher icon."""
 
471
        self.emblem_visible = False
 
472
        self.launcher.hide_emblem()
 
473
 
 
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()
421
478
 
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)
425
484
 
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."""
438
502
        self.cleanup()
439
503
        self.visible = False
 
504
        self.launcher.hide_progressbar()
 
505
        logger.debug("progressbar hidden")
 
506
        if self.inhibitor_defer is not None:
 
507
 
 
508
            def inhibitor_callback(inhibitor):
 
509
                """The inhibitor was found, so cancel it."""
 
510
                self.inhibitor_defer = None
 
511
                return inhibitor.cancel()
 
512
 
 
513
            self.inhibitor_defer.addCallback(inhibitor_callback)
440
514
 
441
515
 
442
516
class FinalStatusBubble(object):
453
527
 
454
528
    def show(self):
455
529
        """Show the final status notification."""
456
 
        self.notification = Notification()
 
530
        self.notification = self.status_aggregator.build_notification()
457
531
        text = self.status_aggregator.get_final_status_message()
458
532
        self.notification.send_notification(UBUNTUONE_TITLE, text)
459
533
 
475
549
    def __init__(self, clock=reactor):
476
550
        """Initialize this instance."""
477
551
        self.clock = clock
 
552
        self.notification_switch = NotificationSwitch()
478
553
        self.reset()
479
554
        self.progress_bar = ProgressBar(clock=self.clock)
480
555
 
 
556
    def build_notification(self):
 
557
        """Create a new toggleable notification object."""
 
558
        return self.notification_switch.build_notification()
 
559
 
481
560
    def reset(self):
482
561
        """Reset all counters and notifications."""
483
562
        self.total_counter = 0
507
586
        lines = []
508
587
 
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)
512
592
 
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)
516
598
 
517
599
        return "\n".join(lines)
521
603
        assert self.total_counter > 0
522
604
        parts = []
523
605
        if self.upload_total:
524
 
            parts.append(PROGRESS_UPLOADED % (self.upload_done,
525
 
                                              self.upload_total))
 
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)
532
615
 
533
616
    def get_final_status_message(self):
534
617
        """Get some lines describing all we did."""
535
618
        parts = []
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)
542
627
 
543
628
    def restart_progress_bubble(self):
546
631
 
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()
551
637
        self.reset()
552
638
 
553
 
    def no_callback(self, status_events):
554
 
        """Pushed a bunch of events by the buffer."""
555
 
 
556
 
        lines = []
557
 
 
558
 
        if self.show_progress:
559
 
            lines.append(self.get_progress_message())
560
 
 
561
 
        for weight, statuses in group_statuses(status_events):
562
 
            statuses = list(statuses)
563
 
            count = len(statuses)
564
 
            first_status = statuses[0]
565
 
            if count == 1:
566
 
                lines.append(first_status.one())
567
 
            else:
568
 
                lines.append(first_status.many(count))
569
 
 
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
574
 
 
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)
620
684
 
 
685
    def connection_lost(self):
 
686
        """The connection to the server was lost."""
 
687
        self.progress_bar.show_warning_emblem()
 
688
 
 
689
    def connection_made(self):
 
690
        """The connection to the server was made."""
 
691
        self.progress_bar.hide_emblem()
 
692
 
621
693
 
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
631
703
 
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())
636
709
 
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())
675
748
 
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,
683
756
            message_count=1)
684
757
        self.notification.send_notification(
685
 
            UBUNTUONE_TITLE, UDFAvailableStatus(udf).one())
 
758
            UBUNTUONE_TITLE, UDFAvailableStatus(udf=udf).one())
686
759
 
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()
697
771
 
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()
 
778
 
 
779
    def set_show_all_notifications(self, value):
 
780
        """Set the flag to show all notifications."""
 
781
        if value:
 
782
            self.aggregator.notification_switch.enable_notifications()
 
783
        else:
 
784
            self.aggregator.notification_switch.disable_notifications()