Home | History | Annotate | Download | only in drive
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "ash/system/drive/tray_drive.h"
      6 
      7 #include <vector>
      8 
      9 #include "ash/shell.h"
     10 #include "ash/system/tray/fixed_sized_scroll_view.h"
     11 #include "ash/system/tray/hover_highlight_view.h"
     12 #include "ash/system/tray/system_tray.h"
     13 #include "ash/system/tray/system_tray_delegate.h"
     14 #include "ash/system/tray/system_tray_notifier.h"
     15 #include "ash/system/tray/tray_constants.h"
     16 #include "ash/system/tray/tray_details_view.h"
     17 #include "ash/system/tray/tray_item_more.h"
     18 #include "ash/system/tray/tray_item_view.h"
     19 #include "base/logging.h"
     20 #include "base/stl_util.h"
     21 #include "base/strings/string_number_conversions.h"
     22 #include "base/strings/utf_string_conversions.h"
     23 #include "grit/ash_resources.h"
     24 #include "grit/ash_strings.h"
     25 #include "ui/base/l10n/l10n_util.h"
     26 #include "ui/base/resource/resource_bundle.h"
     27 #include "ui/gfx/font.h"
     28 #include "ui/gfx/image/image.h"
     29 #include "ui/views/controls/button/image_button.h"
     30 #include "ui/views/controls/image_view.h"
     31 #include "ui/views/controls/label.h"
     32 #include "ui/views/controls/progress_bar.h"
     33 #include "ui/views/layout/box_layout.h"
     34 #include "ui/views/layout/grid_layout.h"
     35 #include "ui/views/widget/widget.h"
     36 
     37 namespace ash {
     38 
     39 namespace internal {
     40 
     41 namespace {
     42 
     43 const int kSidePadding = 8;
     44 const int kHorizontalPadding = 6;
     45 const int kVerticalPadding = 6;
     46 const int kTopPadding = 6;
     47 const int kBottomPadding = 10;
     48 const int kProgressBarWidth = 100;
     49 const int kProgressBarHeight = 11;
     50 const int64 kHideDelayInMs = 1000;
     51 
     52 base::string16 GetTrayLabel(const ash::DriveOperationStatusList& list) {
     53   return l10n_util::GetStringFUTF16(IDS_ASH_STATUS_TRAY_DRIVE_SYNCING,
     54       base::IntToString16(static_cast<int>(list.size())));
     55 }
     56 
     57 scoped_ptr<ash::DriveOperationStatusList> GetCurrentOperationList() {
     58   ash::SystemTrayDelegate* delegate =
     59       ash::Shell::GetInstance()->system_tray_delegate();
     60   scoped_ptr<ash::DriveOperationStatusList> list(
     61       new ash::DriveOperationStatusList);
     62   delegate->GetDriveOperationStatusList(list.get());
     63   return list.Pass();
     64 }
     65 
     66 }
     67 
     68 namespace tray {
     69 
     70 class DriveDefaultView : public TrayItemMore {
     71  public:
     72   DriveDefaultView(SystemTrayItem* owner,
     73                    const DriveOperationStatusList* list)
     74       : TrayItemMore(owner, true) {
     75     ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
     76 
     77     SetImage(bundle.GetImageNamed(IDR_AURA_UBER_TRAY_DRIVE).ToImageSkia());
     78     Update(list);
     79   }
     80 
     81   virtual ~DriveDefaultView() {}
     82 
     83   void Update(const DriveOperationStatusList* list) {
     84     DCHECK(list);
     85     base::string16 label = GetTrayLabel(*list);
     86     SetLabel(label);
     87     SetAccessibleName(label);
     88   }
     89 
     90  private:
     91   DISALLOW_COPY_AND_ASSIGN(DriveDefaultView);
     92 };
     93 
     94 class DriveDetailedView : public TrayDetailsView,
     95                           public ViewClickListener {
     96  public:
     97   DriveDetailedView(SystemTrayItem* owner,
     98                     const DriveOperationStatusList* list)
     99       : TrayDetailsView(owner),
    100         settings_(NULL),
    101         in_progress_img_(NULL),
    102         done_img_(NULL),
    103         failed_img_(NULL) {
    104     in_progress_img_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
    105         IDR_AURA_UBER_TRAY_DRIVE);
    106     done_img_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
    107         IDR_AURA_UBER_TRAY_DRIVE_DONE);
    108     failed_img_ = ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
    109         IDR_AURA_UBER_TRAY_DRIVE_FAILED);
    110 
    111     Update(list);
    112   }
    113 
    114   virtual ~DriveDetailedView() {
    115     STLDeleteValues(&update_map_);
    116   }
    117 
    118   void Update(const DriveOperationStatusList* list) {
    119     AppendOperationList(list);
    120     AppendSettings();
    121     AppendHeaderEntry(list);
    122 
    123     SchedulePaint();
    124   }
    125 
    126  private:
    127 
    128   class OperationProgressBar : public views::ProgressBar {
    129    public:
    130     OperationProgressBar() {}
    131    private:
    132 
    133     // Overridden from View:
    134     virtual gfx::Size GetPreferredSize() OVERRIDE {
    135       return gfx::Size(kProgressBarWidth, kProgressBarHeight);
    136     }
    137 
    138     DISALLOW_COPY_AND_ASSIGN(OperationProgressBar);
    139   };
    140 
    141   class RowView : public HoverHighlightView,
    142                   public views::ButtonListener {
    143    public:
    144     RowView(DriveDetailedView* parent,
    145             ash::DriveOperationStatus::OperationState state,
    146             double progress,
    147             const base::FilePath& file_path,
    148             int32 operation_id)
    149         : HoverHighlightView(parent),
    150           container_(parent),
    151           status_img_(NULL),
    152           label_container_(NULL),
    153           progress_bar_(NULL),
    154           cancel_button_(NULL),
    155           operation_id_(operation_id) {
    156       // Status image.
    157       status_img_ = new views::ImageView();
    158       AddChildView(status_img_);
    159 
    160       label_container_ = new views::View();
    161       label_container_->SetLayoutManager(new views::BoxLayout(
    162           views::BoxLayout::kVertical, 0, 0, kVerticalPadding));
    163 #if defined(OS_POSIX)
    164       base::string16 file_label = UTF8ToUTF16(file_path.BaseName().value());
    165 #elif defined(OS_WIN)
    166       base::string16 file_label = WideToUTF16(file_path.BaseName().value());
    167 #endif
    168       views::Label* label = new views::Label(file_label);
    169       label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    170       label_container_->AddChildView(label);
    171       // Add progress bar.
    172       progress_bar_ = new OperationProgressBar();
    173       label_container_->AddChildView(progress_bar_);
    174 
    175       AddChildView(label_container_);
    176 
    177       cancel_button_ = new views::ImageButton(this);
    178       cancel_button_->SetImage(views::ImageButton::STATE_NORMAL,
    179           ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
    180               IDR_AURA_UBER_TRAY_DRIVE_CANCEL));
    181       cancel_button_->SetImage(views::ImageButton::STATE_HOVERED,
    182           ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
    183               IDR_AURA_UBER_TRAY_DRIVE_CANCEL_HOVER));
    184 
    185       UpdateStatus(state, progress);
    186       AddChildView(cancel_button_);
    187     }
    188 
    189     void UpdateStatus(ash::DriveOperationStatus::OperationState state,
    190                       double progress) {
    191       status_img_->SetImage(container_->GetImageForState(state));
    192       progress_bar_->SetValue(progress);
    193       cancel_button_->SetVisible(
    194           state == ash::DriveOperationStatus::OPERATION_NOT_STARTED ||
    195           state == ash::DriveOperationStatus::OPERATION_IN_PROGRESS);
    196     }
    197 
    198    private:
    199 
    200     // views::View overrides.
    201     virtual gfx::Size GetPreferredSize() OVERRIDE {
    202       return gfx::Size(
    203           status_img_->GetPreferredSize().width() +
    204           label_container_->GetPreferredSize().width() +
    205           cancel_button_->GetPreferredSize().width() +
    206               2 * kSidePadding + 2 * kHorizontalPadding,
    207           std::max(status_img_->GetPreferredSize().height(),
    208                    std::max(label_container_->GetPreferredSize().height(),
    209                             cancel_button_->GetPreferredSize().height())) +
    210                    kTopPadding + kBottomPadding);
    211     }
    212 
    213     virtual void Layout() OVERRIDE {
    214       gfx::Rect child_area(GetLocalBounds());
    215       if (child_area.IsEmpty())
    216         return;
    217 
    218       int pos_x = child_area.x() + kSidePadding;
    219       int pos_y = child_area.y() + kTopPadding;
    220 
    221       gfx::Rect bounds_status(
    222           gfx::Point(pos_x,
    223                      pos_y + (child_area.height() - kTopPadding -
    224                          kBottomPadding -
    225                          status_img_->GetPreferredSize().height())/2),
    226           status_img_->GetPreferredSize());
    227       status_img_->SetBoundsRect(
    228           gfx::IntersectRects(bounds_status, child_area));
    229       pos_x += status_img_->bounds().width() + kHorizontalPadding;
    230 
    231       gfx::Rect bounds_label(pos_x,
    232                              pos_y,
    233                              child_area.width() - 2 * kSidePadding -
    234                                  2 * kHorizontalPadding -
    235                                  status_img_->GetPreferredSize().width() -
    236                                  cancel_button_->GetPreferredSize().width(),
    237                              label_container_->GetPreferredSize().height());
    238       label_container_->SetBoundsRect(
    239           gfx::IntersectRects(bounds_label, child_area));
    240       pos_x += label_container_->bounds().width() + kHorizontalPadding;
    241 
    242       gfx::Rect bounds_button(
    243           gfx::Point(pos_x,
    244                      pos_y + (child_area.height() - kTopPadding -
    245                          kBottomPadding -
    246                          cancel_button_->GetPreferredSize().height())/2),
    247           cancel_button_->GetPreferredSize());
    248       cancel_button_->SetBoundsRect(
    249           gfx::IntersectRects(bounds_button, child_area));
    250     }
    251 
    252     // views::ButtonListener overrides.
    253     virtual void ButtonPressed(views::Button* sender,
    254                                const ui::Event& event) OVERRIDE {
    255       DCHECK(sender == cancel_button_);
    256       container_->OnCancelOperation(operation_id_);
    257     }
    258 
    259     DriveDetailedView* container_;
    260     views::ImageView* status_img_;
    261     views::View* label_container_;
    262     views::ProgressBar* progress_bar_;
    263     views::ImageButton* cancel_button_;
    264     int32 operation_id_;
    265 
    266     DISALLOW_COPY_AND_ASSIGN(RowView);
    267   };
    268 
    269   void AppendHeaderEntry(const DriveOperationStatusList* list) {
    270     if (footer())
    271       return;
    272     CreateSpecialRow(IDS_ASH_STATUS_TRAY_DRIVE, this);
    273   }
    274 
    275   gfx::ImageSkia* GetImageForState(
    276       ash::DriveOperationStatus::OperationState state) {
    277     switch (state) {
    278       case ash::DriveOperationStatus::OPERATION_NOT_STARTED:
    279       case ash::DriveOperationStatus::OPERATION_IN_PROGRESS:
    280         return in_progress_img_;
    281       case ash::DriveOperationStatus::OPERATION_COMPLETED:
    282         return done_img_;
    283       case ash::DriveOperationStatus::OPERATION_FAILED:
    284         return failed_img_;
    285     }
    286     return failed_img_;
    287   }
    288 
    289   void OnCancelOperation(int32 operation_id) {
    290     SystemTrayDelegate* delegate = Shell::GetInstance()->system_tray_delegate();
    291     delegate->CancelDriveOperation(operation_id);
    292   }
    293 
    294   void AppendOperationList(const DriveOperationStatusList* list) {
    295     if (!scroller())
    296       CreateScrollableList();
    297 
    298     // Apply the update.
    299     std::set<base::FilePath> new_set;
    300     bool item_list_changed = false;
    301     for (DriveOperationStatusList::const_iterator it = list->begin();
    302          it != list->end(); ++it) {
    303       const DriveOperationStatus& operation = *it;
    304 
    305       new_set.insert(operation.file_path);
    306       std::map<base::FilePath, RowView*>::iterator existing_item =
    307           update_map_.find(operation.file_path);
    308 
    309       if (existing_item != update_map_.end()) {
    310         existing_item->second->UpdateStatus(operation.state,
    311                                             operation.progress);
    312       } else {
    313         RowView* row_view = new RowView(this,
    314                                         operation.state,
    315                                         operation.progress,
    316                                         operation.file_path,
    317                                         operation.id);
    318 
    319         update_map_[operation.file_path] = row_view;
    320         scroll_content()->AddChildView(row_view);
    321         item_list_changed = true;
    322       }
    323     }
    324 
    325     // Remove items from the list that haven't been added or modified with this
    326     // update batch.
    327     std::set<base::FilePath> remove_set;
    328     for (std::map<base::FilePath, RowView*>::iterator update_iter =
    329              update_map_.begin();
    330          update_iter != update_map_.end(); ++update_iter) {
    331       if (new_set.find(update_iter->first) == new_set.end()) {
    332         remove_set.insert(update_iter->first);
    333       }
    334     }
    335 
    336     for (std::set<base::FilePath>::iterator removed_iter = remove_set.begin();
    337         removed_iter != remove_set.end(); ++removed_iter)  {
    338       delete update_map_[*removed_iter];
    339       update_map_.erase(*removed_iter);
    340       item_list_changed = true;
    341     }
    342 
    343     if (item_list_changed)
    344       scroller()->Layout();
    345 
    346     // Close the details if there is really nothing to show there anymore.
    347     if (new_set.empty() && GetWidget())
    348       GetWidget()->Close();
    349   }
    350 
    351   void AppendSettings() {
    352     if (settings_)
    353       return;
    354 
    355     HoverHighlightView* container = new HoverHighlightView(this);
    356     container->AddLabel(ui::ResourceBundle::GetSharedInstance().
    357         GetLocalizedString(IDS_ASH_STATUS_TRAY_DRIVE_SETTINGS),
    358         gfx::Font::NORMAL);
    359     AddChildView(container);
    360     settings_ = container;
    361   }
    362 
    363   // Overridden from ViewClickListener.
    364   virtual void OnViewClicked(views::View* sender) OVERRIDE {
    365     SystemTrayDelegate* delegate = Shell::GetInstance()->system_tray_delegate();
    366     if (sender == footer()->content()) {
    367       owner()->system_tray()->ShowDefaultView(BUBBLE_USE_EXISTING);
    368     } else if (sender == settings_) {
    369       delegate->ShowDriveSettings();
    370     }
    371   }
    372 
    373   // Maps operation entries to their file paths.
    374   std::map<base::FilePath, RowView*> update_map_;
    375   views::View* settings_;
    376   gfx::ImageSkia* in_progress_img_;
    377   gfx::ImageSkia* done_img_;
    378   gfx::ImageSkia* failed_img_;
    379 
    380   DISALLOW_COPY_AND_ASSIGN(DriveDetailedView);
    381 };
    382 
    383 }  // namespace tray
    384 
    385 TrayDrive::TrayDrive(SystemTray* system_tray) :
    386     TrayImageItem(system_tray, IDR_AURA_UBER_TRAY_DRIVE_LIGHT),
    387     default_(NULL),
    388     detailed_(NULL) {
    389   Shell::GetInstance()->system_tray_notifier()->AddDriveObserver(this);
    390 }
    391 
    392 TrayDrive::~TrayDrive() {
    393   Shell::GetInstance()->system_tray_notifier()->RemoveDriveObserver(this);
    394 }
    395 
    396 bool TrayDrive::GetInitialVisibility() {
    397   return false;
    398 }
    399 
    400 views::View* TrayDrive::CreateDefaultView(user::LoginStatus status) {
    401   DCHECK(!default_);
    402 
    403   if (status != user::LOGGED_IN_USER && status != user::LOGGED_IN_OWNER)
    404     return NULL;
    405 
    406   // If the list is empty AND the tray icon is invisible (= not in the margin
    407   // duration of delayed item hiding), don't show the item.
    408   scoped_ptr<DriveOperationStatusList> list(GetCurrentOperationList());
    409   if (list->empty() && !tray_view()->visible())
    410     return NULL;
    411 
    412   default_ = new tray::DriveDefaultView(this, list.get());
    413   return default_;
    414 }
    415 
    416 views::View* TrayDrive::CreateDetailedView(user::LoginStatus status) {
    417   DCHECK(!detailed_);
    418 
    419   if (status != user::LOGGED_IN_USER && status != user::LOGGED_IN_OWNER)
    420     return NULL;
    421 
    422   // If the list is empty AND the tray icon is invisible (= not in the margin
    423   // duration of delayed item hiding), don't show the item.
    424   scoped_ptr<DriveOperationStatusList> list(GetCurrentOperationList());
    425   if (list->empty() && !tray_view()->visible())
    426     return NULL;
    427 
    428   detailed_ = new tray::DriveDetailedView(this, list.get());
    429   return detailed_;
    430 }
    431 
    432 void TrayDrive::DestroyDefaultView() {
    433   default_ = NULL;
    434 }
    435 
    436 void TrayDrive::DestroyDetailedView() {
    437   detailed_ = NULL;
    438 }
    439 
    440 void TrayDrive::UpdateAfterLoginStatusChange(user::LoginStatus status) {
    441   if (status == user::LOGGED_IN_USER || status == user::LOGGED_IN_OWNER)
    442     return;
    443 
    444   tray_view()->SetVisible(false);
    445   DestroyDefaultView();
    446   DestroyDetailedView();
    447 }
    448 
    449 void TrayDrive::OnDriveJobUpdated(const DriveOperationStatus& status) {
    450   // The Drive job list manager changed its notification interface *not* to send
    451   // the whole list of operations each time, to clarify which operation is
    452   // updated and to reduce redundancy.
    453   //
    454   // TrayDrive should be able to benefit from the change, but for now, to
    455   // incrementally migrate to the new way with minimum diffs, we still get the
    456   // list of operations each time the event is fired.
    457   // TODO(kinaba) http://crbug.com/128079 clean it up.
    458   scoped_ptr<DriveOperationStatusList> list(GetCurrentOperationList());
    459   bool is_new_item = true;
    460   for (size_t i = 0; i < list->size(); ++i) {
    461     if ((*list)[i].id == status.id) {
    462       (*list)[i] = status;
    463       is_new_item = false;
    464       break;
    465     }
    466   }
    467   if (is_new_item)
    468     list->push_back(status);
    469 
    470   // Check if all the operations are in the finished state.
    471   bool all_jobs_finished = true;
    472   for (size_t i = 0; i < list->size(); ++i) {
    473     if ((*list)[i].state != DriveOperationStatus::OPERATION_COMPLETED &&
    474         (*list)[i].state != DriveOperationStatus::OPERATION_FAILED) {
    475       all_jobs_finished = false;
    476       break;
    477     }
    478   }
    479 
    480   if (all_jobs_finished) {
    481     // If all the jobs ended, the tray item will be hidden after a certain
    482     // amount of delay. This is to avoid flashes between sequentially executed
    483     // Drive operations (see crbug/165679).
    484     hide_timer_.Start(FROM_HERE,
    485                       base::TimeDelta::FromMilliseconds(kHideDelayInMs),
    486                       this,
    487                       &TrayDrive::HideIfNoOperations);
    488     return;
    489   }
    490 
    491   // If the list is non-empty, stop the hiding timer (if any).
    492   hide_timer_.Stop();
    493 
    494   tray_view()->SetVisible(true);
    495   if (default_)
    496     default_->Update(list.get());
    497   if (detailed_)
    498     detailed_->Update(list.get());
    499 }
    500 
    501 void TrayDrive::HideIfNoOperations() {
    502   DriveOperationStatusList empty_list;
    503 
    504   tray_view()->SetVisible(false);
    505   if (default_)
    506     default_->Update(&empty_list);
    507   if (detailed_)
    508     detailed_->Update(&empty_list);
    509 }
    510 
    511 }  // namespace internal
    512 }  // namespace ash
    513