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