1 // Copyright (c) 2013 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 "ui/message_center/views/message_popup_collection.h"
6 
7 #include "base/bind.h"
8 #include "base/stl_util.h"
9 #include "base/threading/thread_task_runner_handle.h"
10 #include "ui/gfx/animation/linear_animation.h"
11 #include "ui/gfx/animation/tween.h"
12 #include "ui/message_center/message_center.h"
13 #include "ui/message_center/public/cpp/message_center_constants.h"
14 #include "ui/message_center/views/message_popup_view.h"
15 
16 namespace message_center {
17 
18 namespace {
19 
20 // Animation duration for FADE_IN and FADE_OUT.
21 constexpr base::TimeDelta kFadeInFadeOutDuration =
22     base::TimeDelta::FromMilliseconds(200);
23 
24 // Animation duration for MOVE_DOWN.
25 constexpr base::TimeDelta kMoveDownDuration =
26     base::TimeDelta::FromMilliseconds(120);
27 
28 }  // namespace
29 
MessagePopupCollection()30 MessagePopupCollection::MessagePopupCollection()
31     : animation_(std::make_unique<gfx::LinearAnimation>(this)),
32       weak_ptr_factory_(this) {
33   MessageCenter::Get()->AddObserver(this);
34 }
35 
~MessagePopupCollection()36 MessagePopupCollection::~MessagePopupCollection() {
37   // Ignore calls to update which can cause crashes.
38   is_updating_ = true;
39   for (const auto& item : popup_items_)
40     item.popup->Close();
41   MessageCenter::Get()->RemoveObserver(this);
42 }
43 
Update()44 void MessagePopupCollection::Update() {
45   if (is_updating_)
46     return;
47   base::AutoReset<bool> reset(&is_updating_, true);
48 
49   RemoveClosedPopupItems();
50 
51   if (MessageCenter::Get()->IsMessageCenterVisible()) {
52     CloseAllPopupsNow();
53     return;
54   }
55 
56   if (animation_->is_animating()) {
57     UpdateByAnimation();
58     return;
59   }
60 
61   if (state_ != State::IDLE)
62     TransitionFromAnimation();
63 
64   if (state_ == State::IDLE)
65     TransitionToAnimation();
66 
67   UpdatePopupTimers();
68 
69   if (state_ != State::IDLE) {
70     // If not in IDLE state, start animation.
71     animation_->SetDuration(state_ == State::MOVE_DOWN ||
72                                     state_ == State::MOVE_UP_FOR_INVERSE
73                                 ? kMoveDownDuration
74                                 : kFadeInFadeOutDuration);
75     animation_->Start();
76     UpdateByAnimation();
77   }
78 
79   DCHECK(state_ == State::IDLE || animation_->is_animating());
80 }
81 
ResetBounds()82 void MessagePopupCollection::ResetBounds() {
83   if (is_updating_)
84     return;
85   {
86     base::AutoReset<bool> reset(&is_updating_, true);
87 
88     RemoveClosedPopupItems();
89     ResetHotMode();
90     state_ = State::IDLE;
91     animation_->End();
92 
93     CalculateBounds();
94 
95     // Remove popups that are no longer in work area.
96     ClosePopupsOutsideWorkArea();
97 
98     // Reset bounds and opacity of popups.
99     for (auto& item : popup_items_) {
100       item.popup->SetPopupBounds(item.bounds);
101       item.popup->SetOpacity(1.0);
102     }
103   }
104 
105   // Restart animation for FADE_OUT.
106   Update();
107 }
108 
NotifyPopupResized()109 void MessagePopupCollection::NotifyPopupResized() {
110   resize_requested_ = true;
111   Update();
112 }
113 
NotifyPopupClosed(MessagePopupView * popup)114 void MessagePopupCollection::NotifyPopupClosed(MessagePopupView* popup) {
115   for (auto& item : popup_items_) {
116     if (item.popup == popup)
117       item.popup = nullptr;
118   }
119 }
120 
OnNotificationAdded(const std::string & notification_id)121 void MessagePopupCollection::OnNotificationAdded(
122     const std::string& notification_id) {
123   // Should not call MessagePopupCollection::Update here. Because notification
124   // may be removed before animation which is triggered by the previous
125   // operation on MessagePopupCollection ends. As result, when a new
126   // notification with the same ID is created, calling
127   // MessagePopupCollection::Update will not update the popup's content. Then
128   // the new notification popup fails to show. (see https://crbug.com/921402)
129   OnNotificationUpdated(notification_id);
130 }
131 
OnNotificationRemoved(const std::string & notification_id,bool by_user)132 void MessagePopupCollection::OnNotificationRemoved(
133     const std::string& notification_id,
134     bool by_user) {
135   Update();
136 }
137 
OnNotificationUpdated(const std::string & notification_id)138 void MessagePopupCollection::OnNotificationUpdated(
139     const std::string& notification_id) {
140   if (is_updating_)
141     return;
142 
143   // Find Notification object with |notification_id|.
144   const auto& notifications = GetPopupNotifications();
145   auto it = notifications.begin();
146   while (it != notifications.end()) {
147     if ((*it)->id() == notification_id)
148       break;
149     ++it;
150   }
151 
152   if (it == notifications.end()) {
153     // If not found, probably |notification_id| is removed from popups by
154     // timeout.
155     Update();
156     return;
157   }
158 
159   {
160     base::AutoReset<bool> reset(&is_updating_, true);
161 
162     RemoveClosedPopupItems();
163 
164     // Update contents of the notification.
165     for (const auto& item : popup_items_) {
166       if (item.id == notification_id) {
167         item.popup->UpdateContents(**it);
168         break;
169       }
170     }
171   }
172 
173   Update();
174 }
175 
OnCenterVisibilityChanged(Visibility visibility)176 void MessagePopupCollection::OnCenterVisibilityChanged(Visibility visibility) {
177   Update();
178 }
179 
OnBlockingStateChanged(NotificationBlocker * blocker)180 void MessagePopupCollection::OnBlockingStateChanged(
181     NotificationBlocker* blocker) {
182   Update();
183 }
184 
AnimationEnded(const gfx::Animation * animation)185 void MessagePopupCollection::AnimationEnded(const gfx::Animation* animation) {
186   Update();
187 }
188 
AnimationProgressed(const gfx::Animation * animation)189 void MessagePopupCollection::AnimationProgressed(
190     const gfx::Animation* animation) {
191   Update();
192 }
193 
AnimationCanceled(const gfx::Animation * animation)194 void MessagePopupCollection::AnimationCanceled(
195     const gfx::Animation* animation) {
196   Update();
197 }
198 
GetPopupViewForNotificationID(const std::string & notification_id)199 MessagePopupView* MessagePopupCollection::GetPopupViewForNotificationID(
200     const std::string& notification_id) {
201   for (const auto& item : popup_items_) {
202     if (item.id == notification_id)
203       return item.popup;
204   }
205   return nullptr;
206 }
207 
CreatePopup(const Notification & notification)208 MessagePopupView* MessagePopupCollection::CreatePopup(
209     const Notification& notification) {
210   return new MessagePopupView(notification, this);
211 }
212 
RestartPopupTimers()213 void MessagePopupCollection::RestartPopupTimers() {
214   MessageCenter::Get()->RestartPopupTimers();
215 }
216 
PausePopupTimers()217 void MessagePopupCollection::PausePopupTimers() {
218   MessageCenter::Get()->PausePopupTimers();
219 }
220 
TransitionFromAnimation()221 void MessagePopupCollection::TransitionFromAnimation() {
222   DCHECK_NE(state_, State::IDLE);
223   DCHECK(!animation_->is_animating());
224 
225   // The animation of type |state_| is now finished.
226   UpdateByAnimation();
227 
228   // If FADE_OUT animation is finished, remove the animated popup.
229   if (state_ == State::FADE_OUT) {
230     // In inverse mode if the popups are not removed in the order they were
231     // added (the ones on the top are removed while the ones at the bottom stay)
232     // we need to move the remaining popups down. This might happen if the
233     // popups have different TTL.
234     bool move_down_needed = inverse_ && !AreAllAnimatingPopupsFirst();
235     CloseAnimatingPopups();
236     if (move_down_needed) {
237       state_ = State::MOVE_DOWN;
238       MoveDownPopups();
239       return;
240     }
241   }
242 
243   if (state_ == State::FADE_IN || state_ == State::MOVE_DOWN ||
244       (state_ == State::FADE_OUT && popup_items_.empty())) {
245     // If the animation is finished, transition to IDLE.
246     state_ = State::IDLE;
247   } else if (state_ == State::FADE_OUT && !popup_items_.empty()) {
248     if ((HasAddedPopup() && CollapseAllPopups()) || !inverse_) {
249       // If FADE_OUT animation is finished and we still have remaining popups,
250       // we have to MOVE_DOWN them.
251       // If we're going to add a new popup after this MOVE_DOWN, do the collapse
252       // animation at the same time. Otherwise it will take another MOVE_DOWN.
253       state_ = State::MOVE_DOWN;
254       MoveDownPopups();
255     } else {
256       // If there's no collapsable popups and |inverse_| is on, there's nothing
257       // to do after FADE_OUT.
258       state_ = State::IDLE;
259     }
260   } else if (state_ == State::MOVE_UP_FOR_INVERSE) {
261     for (auto& item : popup_items_)
262       item.is_animating = item.will_fade_in;
263     state_ = State::FADE_IN;
264   }
265 }
266 
TransitionToAnimation()267 void MessagePopupCollection::TransitionToAnimation() {
268   DCHECK_EQ(state_, State::IDLE);
269   DCHECK(!animation_->is_animating());
270 
271   if (HasRemovedPopup()) {
272     MarkRemovedPopup();
273 
274     // Start hot mode to allow a user to continually close many notifications.
275     StartHotMode();
276 
277     if (CloseTransparentPopups()) {
278       // If the popup is already transparent, skip FADE_OUT.
279       state_ = State::MOVE_DOWN;
280       MoveDownPopups();
281     } else {
282       state_ = State::FADE_OUT;
283     }
284     return;
285   }
286 
287   if (HasAddedPopup()) {
288     if (CollapseAllPopups()) {
289       // If we had existing popups that weren't collapsed, first show collapsing
290       // animation.
291       state_ = State::MOVE_DOWN;
292       MoveDownPopups();
293       return;
294     } else if (AddPopup()) {
295       // A popup is actually added.
296       if (inverse_ && popup_items_.size() > 1) {
297         // If |inverse_| is on and there are existing notifications that have to
298         // be moved up (existing ones + new one, so > 1), transition to
299         // MOVE_UP_FOR_INVERSE.
300         state_ = State::MOVE_UP_FOR_INVERSE;
301       } else {
302         // Show FADE_IN animation.
303         state_ = State::FADE_IN;
304       }
305       return;
306     }
307   }
308 
309   if (resize_requested_) {
310     // Resize is requested e.g. a user manually expanded notification.
311     resize_requested_ = false;
312     state_ = State::MOVE_DOWN;
313     MoveDownPopups();
314 
315     // This function may be called by a child MessageView when a notification is
316     // expanded by the user.  Deleting the pop-up should be delayed so we are
317     // out of the child view's call stack. See crbug.com/957033.
318     base::ThreadTaskRunnerHandle::Get()->PostTask(
319         FROM_HERE,
320         base::BindOnce(&MessagePopupCollection::ClosePopupsOutsideWorkArea,
321                        weak_ptr_factory_.GetWeakPtr()));
322     return;
323   }
324 
325   if (!IsAnyPopupHovered() && is_hot_) {
326     // Reset hot mode and animate to the normal positions.
327     state_ = State::MOVE_DOWN;
328     ResetHotMode();
329     MoveDownPopups();
330   }
331 }
332 
UpdatePopupTimers()333 void MessagePopupCollection::UpdatePopupTimers() {
334   if (state_ == State::IDLE) {
335     if (IsAnyPopupHovered() || IsAnyPopupActive()) {
336       // If any popup is hovered or activated, pause popup timer.
337       PausePopupTimers();
338     } else {
339       // If in IDLE state, restart popup timer.
340       RestartPopupTimers();
341     }
342   } else {
343     // If not in IDLE state, pause popup timer.
344     PausePopupTimers();
345   }
346 }
347 
CalculateBounds()348 void MessagePopupCollection::CalculateBounds() {
349   int base = GetBaseline();
350   for (size_t i = 0; i < popup_items_.size(); ++i) {
351     gfx::Size preferred_size(
352         kNotificationWidth,
353         GetPopupItem(i)->popup->GetHeightForWidth(kNotificationWidth));
354 
355     // Align the top of i-th popup to |hot_top_|.
356     if (is_hot_ && hot_index_ == i) {
357       base = hot_top_;
358       if (!IsTopDown())
359         base += preferred_size.height();
360     }
361 
362     int origin_x = GetToastOriginX(gfx::Rect(preferred_size));
363 
364     int origin_y = base;
365     if (!IsTopDown())
366       origin_y -= preferred_size.height();
367 
368     GetPopupItem(i)->start_bounds = GetPopupItem(i)->bounds;
369     GetPopupItem(i)->bounds =
370         gfx::Rect(gfx::Point(origin_x, origin_y), preferred_size);
371 
372     const int delta = preferred_size.height() + kMarginBetweenPopups;
373     if (IsTopDown())
374       base += delta;
375     else
376       base -= delta;
377   }
378 }
379 
UpdateByAnimation()380 void MessagePopupCollection::UpdateByAnimation() {
381   DCHECK_NE(state_, State::IDLE);
382 
383   for (auto& item : popup_items_) {
384     if (!item.is_animating)
385       continue;
386 
387     double value = gfx::Tween::CalculateValue(
388         state_ == State::FADE_OUT ? gfx::Tween::EASE_IN : gfx::Tween::EASE_OUT,
389         animation_->GetCurrentValue());
390 
391     if (state_ == State::FADE_IN)
392       item.popup->SetOpacity(gfx::Tween::FloatValueBetween(value, 0.0f, 1.0f));
393     else if (state_ == State::FADE_OUT)
394       item.popup->SetOpacity(gfx::Tween::FloatValueBetween(value, 1.0f, 0.0f));
395 
396     if (state_ == State::FADE_IN || state_ == State::MOVE_DOWN ||
397         state_ == State::MOVE_UP_FOR_INVERSE) {
398       item.popup->SetPopupBounds(
399           gfx::Tween::RectValueBetween(value, item.start_bounds, item.bounds));
400     }
401   }
402 }
403 
GetPopupNotifications() const404 std::vector<Notification*> MessagePopupCollection::GetPopupNotifications()
405     const {
406   std::vector<Notification*> result;
407   for (auto* notification : MessageCenter::Get()->GetPopupNotifications()) {
408     // Disables popup of custom notification on non-primary displays, since
409     // currently custom notification supports only on one display at the same
410     // time.
411     // TODO(yoshiki): Support custom popup notification on multiple display
412     // (https://crbug.com/715370).
413     if (!IsPrimaryDisplayForNotification() &&
414         notification->type() == NOTIFICATION_TYPE_CUSTOM) {
415       continue;
416     }
417 
418     if (BlockForMixedFullscreen(*notification))
419       continue;
420 
421     result.emplace_back(notification);
422   }
423   return result;
424 }
425 
AddPopup()426 bool MessagePopupCollection::AddPopup() {
427   std::set<std::string> existing_ids;
428   for (const auto& item : popup_items_)
429     existing_ids.insert(item.id);
430 
431   auto notifications = GetPopupNotifications();
432   Notification* new_notification = nullptr;
433   // Reverse iterating because notifications are in reverse chronological order.
434   for (auto it = notifications.rbegin(); it != notifications.rend(); ++it) {
435     if (!existing_ids.count((*it)->id())) {
436       new_notification = *it;
437       break;
438     }
439   }
440 
441   if (!new_notification)
442     return false;
443 
444   // Reset animation flags of existing popups.
445   for (auto& item : popup_items_) {
446     item.is_animating = false;
447     item.will_fade_in = false;
448   }
449 
450   {
451     PopupItem item;
452     item.id = new_notification->id();
453     item.is_animating = true;
454     item.popup = CreatePopup(*new_notification);
455 
456     if (IsNextEdgeOutsideWorkArea(item)) {
457       item.popup->Close();
458       return false;
459     }
460 
461     item.popup->Show();
462     popup_items_.push_back(item);
463     NotifyPopupAdded(item.popup);
464   }
465 
466   // There are existing notifications that have to be moved up (existing ones +
467   // new one, so > 1).
468   if (inverse_ && popup_items_.size() > 1) {
469     for (auto& item : popup_items_) {
470       item.will_fade_in = item.is_animating;
471       item.is_animating = !item.is_animating;
472     }
473   }
474 
475   MessageCenter::Get()->DisplayedNotification(new_notification->id(),
476                                               DISPLAY_SOURCE_POPUP);
477 
478   CalculateBounds();
479 
480   auto& item = popup_items_.back();
481   item.start_bounds = item.bounds;
482   item.start_bounds +=
483       gfx::Vector2d((IsFromLeft() ? -1 : 1) * item.bounds.width(), 0);
484   return true;
485 }
486 
MarkRemovedPopup()487 void MessagePopupCollection::MarkRemovedPopup() {
488   std::set<std::string> existing_ids;
489   for (Notification* notification : GetPopupNotifications()) {
490     existing_ids.insert(notification->id());
491   }
492 
493   for (auto& item : popup_items_) {
494     bool removing = !existing_ids.count(item.id);
495     item.is_animating = removing;
496     if (removing)
497       NotifyPopupRemoved(item.id);
498   }
499 }
500 
MoveDownPopups()501 void MessagePopupCollection::MoveDownPopups() {
502   CalculateBounds();
503   for (auto& item : popup_items_)
504     item.is_animating = true;
505 }
506 
GetNextEdge(const PopupItem & item) const507 int MessagePopupCollection::GetNextEdge(const PopupItem& item) const {
508   const int delta =
509       item.popup->GetHeightForWidth(kNotificationWidth) + kMarginBetweenPopups;
510 
511   int base = 0;
512   if (popup_items_.empty()) {
513     base = GetBaseline();
514   } else if (inverse_) {
515     base = IsTopDown() ? popup_items_.front().bounds.bottom()
516                        : popup_items_.front().bounds.y();
517   } else {
518     base = IsTopDown() ? popup_items_.back().bounds.bottom()
519                        : popup_items_.back().bounds.y();
520   }
521 
522   return IsTopDown() ? base + delta : base - delta;
523 }
524 
IsNextEdgeOutsideWorkArea(const PopupItem & item) const525 bool MessagePopupCollection::IsNextEdgeOutsideWorkArea(
526     const PopupItem& item) const {
527   const int next_edge = GetNextEdge(item);
528   const gfx::Rect work_area = GetWorkArea();
529   return IsTopDown() ? next_edge > work_area.bottom()
530                      : next_edge < work_area.y();
531 }
532 
StartHotMode()533 void MessagePopupCollection::StartHotMode() {
534   for (size_t i = 0; i < popup_items_.size(); ++i) {
535     if (GetPopupItem(i)->is_animating && GetPopupItem(i)->popup->is_hovered()) {
536       is_hot_ = true;
537       hot_index_ = i;
538       hot_top_ = GetPopupItem(i)->bounds.y();
539       break;
540     }
541   }
542 }
543 
ResetHotMode()544 void MessagePopupCollection::ResetHotMode() {
545   is_hot_ = false;
546   hot_index_ = 0;
547   hot_top_ = 0;
548 }
549 
AreAllAnimatingPopupsFirst() const550 bool MessagePopupCollection::AreAllAnimatingPopupsFirst() const {
551   bool previous_item_was_animating = true;
552   for (const auto& item : popup_items_) {
553     if (item.is_animating && !previous_item_was_animating)
554       return false;
555     previous_item_was_animating = item.is_animating;
556   }
557   return true;
558 }
559 
CloseAnimatingPopups()560 void MessagePopupCollection::CloseAnimatingPopups() {
561   for (auto& item : popup_items_) {
562     if (!item.is_animating)
563       continue;
564     item.popup->Close();
565   }
566   RemoveClosedPopupItems();
567 }
568 
CloseTransparentPopups()569 bool MessagePopupCollection::CloseTransparentPopups() {
570   bool removed = false;
571   for (auto& item : popup_items_) {
572     if (item.popup->GetOpacity() > 0.0)
573       continue;
574     item.popup->Close();
575     removed = true;
576   }
577   RemoveClosedPopupItems();
578   return removed;
579 }
580 
ClosePopupsOutsideWorkArea()581 void MessagePopupCollection::ClosePopupsOutsideWorkArea() {
582   const gfx::Rect work_area = GetWorkArea();
583   for (auto& item : popup_items_) {
584     if (work_area.Contains(item.bounds))
585       continue;
586     item.popup->Close();
587   }
588   RemoveClosedPopupItems();
589 }
590 
RemoveClosedPopupItems()591 void MessagePopupCollection::RemoveClosedPopupItems() {
592   base::EraseIf(popup_items_, [](const auto& item) { return !item.popup; });
593 }
594 
CloseAllPopupsNow()595 void MessagePopupCollection::CloseAllPopupsNow() {
596   for (auto& item : popup_items_)
597     item.is_animating = true;
598   CloseAnimatingPopups();
599 
600   ResetHotMode();
601   state_ = State::IDLE;
602   animation_->End();
603 }
604 
CollapseAllPopups()605 bool MessagePopupCollection::CollapseAllPopups() {
606   bool changed = false;
607   for (auto& item : popup_items_) {
608     int old_height = item.popup->GetHeightForWidth(kNotificationWidth);
609 
610     item.popup->AutoCollapse();
611 
612     int new_height = item.popup->GetHeightForWidth(kNotificationWidth);
613     if (old_height != new_height)
614       changed = true;
615   }
616 
617   resize_requested_ = false;
618   return changed;
619 }
620 
HasAddedPopup() const621 bool MessagePopupCollection::HasAddedPopup() const {
622   std::set<std::string> existing_ids;
623   for (const auto& item : popup_items_)
624     existing_ids.insert(item.id);
625 
626   for (Notification* notification : GetPopupNotifications()) {
627     if (!existing_ids.count(notification->id()))
628       return true;
629   }
630   return false;
631 }
632 
HasRemovedPopup() const633 bool MessagePopupCollection::HasRemovedPopup() const {
634   std::set<std::string> existing_ids;
635   for (Notification* notification : GetPopupNotifications()) {
636     existing_ids.insert(notification->id());
637   }
638 
639   for (const auto& item : popup_items_) {
640     if (!existing_ids.count(item.id))
641       return true;
642   }
643   return false;
644 }
645 
IsAnyPopupHovered() const646 bool MessagePopupCollection::IsAnyPopupHovered() const {
647   for (const auto& item : popup_items_) {
648     if (item.popup->is_hovered())
649       return true;
650   }
651   return false;
652 }
653 
IsAnyPopupActive() const654 bool MessagePopupCollection::IsAnyPopupActive() const {
655   for (const auto& item : popup_items_) {
656     if (item.popup->is_active())
657       return true;
658   }
659   return false;
660 }
661 
GetPopupItem(size_t index_from_top)662 MessagePopupCollection::PopupItem* MessagePopupCollection::GetPopupItem(
663     size_t index_from_top) {
664   DCHECK_LT(index_from_top, popup_items_.size());
665   return &popup_items_[inverse_ ? popup_items_.size() - index_from_top - 1
666                                 : index_from_top];
667 }
668 
669 }  // namespace message_center
670