diff options
author | Ben Murdoch <benm@google.com> | 2010-07-29 17:14:53 +0100 |
---|---|---|
committer | Ben Murdoch <benm@google.com> | 2010-08-04 14:29:45 +0100 |
commit | c407dc5cd9bdc5668497f21b26b09d988ab439de (patch) | |
tree | 7eaf8707c0309516bdb042ad976feedaf72b0bb1 /chrome/browser/notifications | |
parent | 0998b1cdac5733f299c12d88bc31ef9c8035b8fa (diff) | |
download | external_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.zip external_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.tar.gz external_chromium-c407dc5cd9bdc5668497f21b26b09d988ab439de.tar.bz2 |
Merge Chromium src@r53293
Change-Id: Ia79acf8670f385cee48c45b0a75371d8e950af34
Diffstat (limited to 'chrome/browser/notifications')
31 files changed, 3908 insertions, 0 deletions
diff --git a/chrome/browser/notifications/balloon.cc b/chrome/browser/notifications/balloon.cc new file mode 100644 index 0000000..969bde1 --- /dev/null +++ b/chrome/browser/notifications/balloon.cc @@ -0,0 +1,64 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/balloon.h" + +#include "base/logging.h" +#include "chrome/browser/notifications/balloon_collection.h" +#include "chrome/browser/notifications/notification.h" +#include "chrome/browser/renderer_host/site_instance.h" +#include "gfx/rect.h" + +Balloon::Balloon(const Notification& notification, Profile* profile, + BalloonCollection* collection) + : profile_(profile), + notification_(new Notification(notification)), + collection_(collection) { +} + +Balloon::~Balloon() { +} + +void Balloon::SetPosition(const gfx::Point& upper_left, bool reposition) { + position_ = upper_left; + if (reposition && balloon_view_.get()) + balloon_view_->RepositionToBalloon(); +} + +void Balloon::SetContentPreferredSize(const gfx::Size& size) { + collection_->ResizeBalloon(this, size); +} + +void Balloon::set_view(BalloonView* balloon_view) { + balloon_view_.reset(balloon_view); +} + +void Balloon::Show() { + notification_->Display(); + if (balloon_view_.get()) { + balloon_view_->Show(this); + balloon_view_->RepositionToBalloon(); + } +} + +void Balloon::Update(const Notification& notification) { + notification_->Close(false); + notification_.reset(new Notification(notification)); + notification_->Display(); + if (balloon_view_.get()) { + balloon_view_->Update(); + } +} + +void Balloon::OnClose(bool by_user) { + notification_->Close(by_user); + collection_->OnBalloonClosed(this); +} + +void Balloon::CloseByScript() { + // A user-initiated close begins with the view and then closes this object; + // we simulate that with a script-initiated close but pass |by_user|=false. + DCHECK(balloon_view_.get()); + balloon_view_->Close(false); +} diff --git a/chrome/browser/notifications/balloon.h b/chrome/browser/notifications/balloon.h new file mode 100644 index 0000000..7f3b604 --- /dev/null +++ b/chrome/browser/notifications/balloon.h @@ -0,0 +1,134 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Handles the visible notification (or balloons). + +#ifndef CHROME_BROWSER_NOTIFICATIONS_BALLOON_H_ +#define CHROME_BROWSER_NOTIFICATIONS_BALLOON_H_ + +#include <vector> + +#include "base/basictypes.h" +#include "base/scoped_ptr.h" +#include "gfx/point.h" +#include "gfx/rect.h" +#include "gfx/size.h" + +class Balloon; +class BalloonCollection; +class BalloonHost; +class Notification; +class Profile; +class SiteInstance; + +// Interface for a view that displays a balloon. +class BalloonView { + public: + virtual ~BalloonView() { } + + // Show the view on the screen. + virtual void Show(Balloon* balloon) = 0; + + // Notify that the content of notification has chagned. + virtual void Update() = 0; + + // Reposition the view to match the position of its balloon. + virtual void RepositionToBalloon() = 0; + + // Close the view. + virtual void Close(bool by_user) = 0; + + // The total size of the view. + virtual gfx::Size GetSize() const = 0; + + // The host for the view's contents. + virtual BalloonHost* GetHost() const = 0; +}; + +// Represents a Notification on the screen. +class Balloon { + public: + Balloon(const Notification& notification, + Profile* profile, + BalloonCollection* collection); + virtual ~Balloon(); + + const Notification& notification() const { return *notification_.get(); } + Profile* profile() const { return profile_; } + + gfx::Point GetPosition() const { + return position_.Add(offset_); + } + void SetPosition(const gfx::Point& upper_left, bool reposition); + + const gfx::Point& offset() { return offset_;} + void set_offset(const gfx::Point& offset) { offset_ = offset; } + void add_offset(const gfx::Point& offset) { offset_ = offset_.Add(offset); } + + const gfx::Size& content_size() const { return content_size_; } + void set_content_size(const gfx::Size& size) { content_size_ = size; } + + const gfx::Size& min_scrollbar_size() const { return min_scrollbar_size_; } + void set_min_scrollbar_size(const gfx::Size& size) { + min_scrollbar_size_ = size; + } + + // Request a new content size for this balloon. This will get passed + // to the balloon collection for checking against available space and + // min/max restrictions. + void SetContentPreferredSize(const gfx::Size& size); + + // Provides a view for this balloon. Ownership transfers + // to this object. + void set_view(BalloonView* balloon_view); + + // Returns the balloon view associated with the balloon. + BalloonView* view() const { + return balloon_view_.get(); + } + + // Returns the viewing size for the balloon (content + frame). + gfx::Size GetViewSize() const { return balloon_view_->GetSize(); } + + // Shows the balloon. + virtual void Show(); + + // Notify that the content of notification has changed. + virtual void Update(const Notification& notification); + + // Called when the balloon is closed, either by user (through the UI) + // or by a script. + virtual void OnClose(bool by_user); + + // Called by script to cause the balloon to close. + virtual void CloseByScript(); + + private: + // Non-owned pointer to the profile. + Profile* profile_; + + // The notification being shown in this balloon. + scoped_ptr<Notification> notification_; + + // The collection that this balloon belongs to. Non-owned pointer. + BalloonCollection* collection_; + + // The actual UI element for the balloon. + scoped_ptr<BalloonView> balloon_view_; + + // Position and size of the balloon on the screen. + gfx::Point position_; + gfx::Size content_size_; + + // Temporary offset for balloons that need to be positioned in a non-standard + // position for keeping the close buttons under the mouse cursor. + gfx::Point offset_; + + // Smallest size for this balloon where scrollbars will be shown. + gfx::Size min_scrollbar_size_; + + DISALLOW_COPY_AND_ASSIGN(Balloon); +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_BALLOON_H_ diff --git a/chrome/browser/notifications/balloon_collection.cc b/chrome/browser/notifications/balloon_collection.cc new file mode 100644 index 0000000..3b66a07 --- /dev/null +++ b/chrome/browser/notifications/balloon_collection.cc @@ -0,0 +1,321 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/balloon_collection_impl.h" + +#include "base/logging.h" +#include "base/stl_util-inl.h" +#include "chrome/browser/notifications/balloon.h" +#include "chrome/browser/notifications/balloon_host.h" +#include "chrome/browser/notifications/notification.h" +#include "chrome/browser/window_sizer.h" +#include "gfx/rect.h" +#include "gfx/size.h" + +namespace { + +// Portion of the screen allotted for notifications. When notification balloons +// extend over this, no new notifications are shown until some are closed. +const double kPercentBalloonFillFactor = 0.7; + +// Allow at least this number of balloons on the screen. +const int kMinAllowedBalloonCount = 2; + +// Delay from the mouse leaving the balloon collection before +// there is a relayout, in milliseconds. +const int kRepositionDelay = 300; + +} // namespace + +// static +// Note that on MacOS, since the coordinate system is inverted vertically from +// the others, this actually produces notifications coming from the TOP right, +// which is what is desired. +BalloonCollectionImpl::Layout::Placement + BalloonCollectionImpl::Layout::placement_ = + Layout::VERTICALLY_FROM_BOTTOM_RIGHT; + +BalloonCollectionImpl::BalloonCollectionImpl() +#if USE_OFFSETS + : ALLOW_THIS_IN_INITIALIZER_LIST(reposition_factory_(this)), + added_as_message_loop_observer_(false) +#endif +{ +} + +BalloonCollectionImpl::~BalloonCollectionImpl() { + STLDeleteElements(&balloons_); +} + +void BalloonCollectionImpl::Add(const Notification& notification, + Profile* profile) { + Balloon* new_balloon = MakeBalloon(notification, profile); + // The +1 on width is necessary because width is fixed on notifications, + // so since we always have the max size, we would always hit the scrollbar + // condition. We are only interested in comparing height to maximum. + new_balloon->set_min_scrollbar_size(gfx::Size(1 + layout_.max_balloon_width(), + layout_.max_balloon_height())); + new_balloon->SetPosition(layout_.OffScreenLocation(), false); + new_balloon->Show(); +#if USE_OFFSETS + if (balloons_.size() > 0) + new_balloon->set_offset(balloons_[balloons_.size() - 1]->offset()); +#endif + + balloons_.push_back(new_balloon); + PositionBalloons(false); + + // There may be no listener in a unit test. + if (space_change_listener_) + space_change_listener_->OnBalloonSpaceChanged(); +} + +bool BalloonCollectionImpl::Remove(const Notification& notification) { + Balloons::iterator iter; + for (iter = balloons_.begin(); iter != balloons_.end(); ++iter) { + if (notification.IsSame((*iter)->notification())) { + // Balloon.CloseByScript() will cause OnBalloonClosed() to be called on + // this object, which will remove it from the collection and free it. + (*iter)->CloseByScript(); + return true; + } + } + return false; +} + +bool BalloonCollectionImpl::HasSpace() const { + if (count() < kMinAllowedBalloonCount) + return true; + + int max_balloon_size = 0; + int total_size = 0; + layout_.GetMaxLinearSize(&max_balloon_size, &total_size); + + int current_max_size = max_balloon_size * count(); + int max_allowed_size = static_cast<int>(total_size * + kPercentBalloonFillFactor); + return current_max_size < max_allowed_size - max_balloon_size; +} + +void BalloonCollectionImpl::ResizeBalloon(Balloon* balloon, + const gfx::Size& size) { + balloon->set_content_size(Layout::ConstrainToSizeLimits(size)); + PositionBalloons(true); +} + +void BalloonCollectionImpl::DisplayChanged() { + layout_.RefreshSystemMetrics(); + PositionBalloons(true); +} + +void BalloonCollectionImpl::OnBalloonClosed(Balloon* source) { + // We want to free the balloon when finished. + scoped_ptr<Balloon> closed(source); + Balloons::iterator it = balloons_.begin(); + +#if USE_OFFSETS + gfx::Point offset; + bool apply_offset = false; + while (it != balloons_.end()) { + if (*it == source) { + it = balloons_.erase(it); + if (it != balloons_.end()) { + apply_offset = true; + offset.set_y((source)->offset().y() - (*it)->offset().y() + + (*it)->content_size().height() - source->content_size().height()); + } + } else { + if (apply_offset) + (*it)->add_offset(offset); + ++it; + } + } + // Start listening for UI events so we cancel the offset when the mouse + // leaves the balloon area. + if (apply_offset) + AddMessageLoopObserver(); +#else + for (; it != balloons_.end(); ++it) { + if (*it == source) { + balloons_.erase(it); + break; + } + } +#endif + + PositionBalloons(true); + + // There may be no listener in a unit test. + if (space_change_listener_) + space_change_listener_->OnBalloonSpaceChanged(); +} + +void BalloonCollectionImpl::PositionBalloons(bool reposition) { + layout_.RefreshSystemMetrics(); + gfx::Point origin = layout_.GetLayoutOrigin(); + for (Balloons::iterator it = balloons_.begin(); it != balloons_.end(); ++it) { + gfx::Point upper_left = layout_.NextPosition((*it)->GetViewSize(), &origin); + (*it)->SetPosition(upper_left, reposition); + } +} + +#if USE_OFFSETS +void BalloonCollectionImpl::AddMessageLoopObserver() { + if (!added_as_message_loop_observer_) { + MessageLoopForUI::current()->AddObserver(this); + added_as_message_loop_observer_ = true; + } +} + +void BalloonCollectionImpl::RemoveMessageLoopObserver() { + if (added_as_message_loop_observer_) { + MessageLoopForUI::current()->RemoveObserver(this); + added_as_message_loop_observer_ = false; + } +} + +void BalloonCollectionImpl::CancelOffsets() { + reposition_factory_.RevokeAll(); + + // Unhook from listening to all UI events. + RemoveMessageLoopObserver(); + + for (Balloons::iterator it = balloons_.begin(); it != balloons_.end(); ++it) + (*it)->set_offset(gfx::Point(0, 0)); + + PositionBalloons(true); +} + +void BalloonCollectionImpl::HandleMouseMoveEvent() { + if (!IsCursorInBalloonCollection()) { + // Mouse has left the region. Schedule a reposition after + // a short delay. + if (reposition_factory_.empty()) { + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + reposition_factory_.NewRunnableMethod( + &BalloonCollectionImpl::CancelOffsets), + kRepositionDelay); + } + } else { + // Mouse moved back into the region. Cancel the reposition. + reposition_factory_.RevokeAll(); + } +} +#endif + +BalloonCollectionImpl::Layout::Layout() { + RefreshSystemMetrics(); +} + +void BalloonCollectionImpl::Layout::GetMaxLinearSize(int* max_balloon_size, + int* total_size) const { + DCHECK(max_balloon_size && total_size); + + switch (placement_) { + case VERTICALLY_FROM_TOP_RIGHT: + case VERTICALLY_FROM_BOTTOM_RIGHT: + *total_size = work_area_.height(); + *max_balloon_size = max_balloon_height(); + break; + default: + NOTREACHED(); + break; + } +} + +gfx::Point BalloonCollectionImpl::Layout::GetLayoutOrigin() const { + int x = 0; + int y = 0; + switch (placement_) { + case VERTICALLY_FROM_TOP_RIGHT: + x = work_area_.right() - HorizontalEdgeMargin(); + y = work_area_.y() + VerticalEdgeMargin(); + break; + case VERTICALLY_FROM_BOTTOM_RIGHT: + x = work_area_.right() - HorizontalEdgeMargin(); + y = work_area_.bottom() - VerticalEdgeMargin(); + break; + default: + NOTREACHED(); + break; + } + return gfx::Point(x, y); +} + +gfx::Point BalloonCollectionImpl::Layout::NextPosition( + const gfx::Size& balloon_size, + gfx::Point* position_iterator) const { + DCHECK(position_iterator); + + int x = 0; + int y = 0; + switch (placement_) { + case VERTICALLY_FROM_TOP_RIGHT: + x = position_iterator->x() - balloon_size.width(); + y = position_iterator->y(); + position_iterator->set_y(position_iterator->y() + balloon_size.height() + + InterBalloonMargin()); + break; + case VERTICALLY_FROM_BOTTOM_RIGHT: + position_iterator->set_y(position_iterator->y() - balloon_size.height() - + InterBalloonMargin()); + x = position_iterator->x() - balloon_size.width(); + y = position_iterator->y(); + break; + default: + NOTREACHED(); + break; + } + return gfx::Point(x, y); +} + +gfx::Point BalloonCollectionImpl::Layout::OffScreenLocation() const { + int x = 0; + int y = 0; + switch (placement_) { + case VERTICALLY_FROM_TOP_RIGHT: + x = work_area_.right() - kBalloonMaxWidth - HorizontalEdgeMargin(); + y = work_area_.y() + kBalloonMaxHeight + VerticalEdgeMargin(); + break; + case VERTICALLY_FROM_BOTTOM_RIGHT: + x = work_area_.right() - kBalloonMaxWidth - HorizontalEdgeMargin(); + y = work_area_.bottom() + kBalloonMaxHeight + VerticalEdgeMargin(); + break; + default: + NOTREACHED(); + break; + } + return gfx::Point(x, y); +} + +// static +gfx::Size BalloonCollectionImpl::Layout::ConstrainToSizeLimits( + const gfx::Size& size) { + // restrict to the min & max sizes + return gfx::Size( + std::max(min_balloon_width(), + std::min(max_balloon_width(), size.width())), + std::max(min_balloon_height(), + std::min(max_balloon_height(), size.height()))); +} + +bool BalloonCollectionImpl::Layout::RefreshSystemMetrics() { + bool changed = false; + +#if defined(OS_MACOSX) + gfx::Rect new_work_area = GetMacWorkArea(); +#else + scoped_ptr<WindowSizer::MonitorInfoProvider> info_provider( + WindowSizer::CreateDefaultMonitorInfoProvider()); + gfx::Rect new_work_area = info_provider->GetPrimaryMonitorWorkArea(); +#endif + if (!work_area_.Equals(new_work_area)) { + work_area_.SetRect(new_work_area.x(), new_work_area.y(), + new_work_area.width(), new_work_area.height()); + changed = true; + } + + return changed; +} diff --git a/chrome/browser/notifications/balloon_collection.h b/chrome/browser/notifications/balloon_collection.h new file mode 100644 index 0000000..2d868c4 --- /dev/null +++ b/chrome/browser/notifications/balloon_collection.h @@ -0,0 +1,75 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Handles the visible notification (or balloons). + +#ifndef CHROME_BROWSER_NOTIFICATIONS_BALLOON_COLLECTION_H_ +#define CHROME_BROWSER_NOTIFICATIONS_BALLOON_COLLECTION_H_ + +#include <deque> + +class Balloon; +class Notification; +class Profile; + +namespace gfx { +class Size; +} // namespace gfx + +class BalloonCollection { + public: + class BalloonSpaceChangeListener { + public: + virtual ~BalloonSpaceChangeListener() {} + + // Called when there is more or less space for balloons due to + // monitor size changes or balloons disappearing. + virtual void OnBalloonSpaceChanged() = 0; + }; + + static BalloonCollection* Create(); + + BalloonCollection() + : space_change_listener_(NULL) { + } + + virtual ~BalloonCollection() {} + + // Adds a new balloon for the specified notification. + virtual void Add(const Notification& notification, + Profile* profile) = 0; + + // Removes a balloon from the collection if present. Returns + // true if anything was removed. + virtual bool Remove(const Notification& notification) = 0; + + // Is there room to add another notification? + virtual bool HasSpace() const = 0; + + // Request the resizing of a balloon. + virtual void ResizeBalloon(Balloon* balloon, const gfx::Size& size) = 0; + + // Update for new screen dimensions. + virtual void DisplayChanged() = 0; + + // Inform the collection that a balloon was closed. + virtual void OnBalloonClosed(Balloon* source) = 0; + + // Get const collection of the active balloons. + typedef std::deque<Balloon*> Balloons; + virtual const Balloons& GetActiveBalloons() = 0; + + BalloonSpaceChangeListener* space_change_listener() { + return space_change_listener_; + } + void set_space_change_listener(BalloonSpaceChangeListener* listener) { + space_change_listener_ = listener; + } + + protected: + // Non-owned pointer to an object listening for space changes. + BalloonSpaceChangeListener* space_change_listener_; +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_BALLOON_COLLECTION_H_ diff --git a/chrome/browser/notifications/balloon_collection_impl.h b/chrome/browser/notifications/balloon_collection_impl.h new file mode 100644 index 0000000..cb06230 --- /dev/null +++ b/chrome/browser/notifications/balloon_collection_impl.h @@ -0,0 +1,181 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Handles the visible notification (or balloons). + +#ifndef CHROME_BROWSER_NOTIFICATIONS_BALLOON_COLLECTION_IMPL_H_ +#define CHROME_BROWSER_NOTIFICATIONS_BALLOON_COLLECTION_IMPL_H_ + +#include <deque> + +#include "base/basictypes.h" +#include "base/message_loop.h" +#include "chrome/browser/notifications/balloon_collection.h" +#include "gfx/point.h" +#include "gfx/rect.h" + +// Mac balloons grow from the top down and have close buttons on top, so +// offsetting is not necessary for easy multiple-closing. Other platforms grow +// from the bottom up and have close buttons on top, so it is necessary. +#if defined(OS_MACOSX) +#define USE_OFFSETS 0 +#else +#define USE_OFFSETS 1 +#endif + +// A balloon collection represents a set of notification balloons being +// shown on the screen. It positions new notifications according to +// a layout, and monitors for balloons being closed, which it reports +// up to its parent, the notification UI manager. +class BalloonCollectionImpl : public BalloonCollection +#if USE_OFFSETS + , public MessageLoopForUI::Observer +#endif +{ + public: + BalloonCollectionImpl(); + virtual ~BalloonCollectionImpl(); + + // BalloonCollection interface. + virtual void Add(const Notification& notification, + Profile* profile); + virtual bool Remove(const Notification& notification); + virtual bool HasSpace() const; + virtual void ResizeBalloon(Balloon* balloon, const gfx::Size& size); + virtual void DisplayChanged(); + virtual void OnBalloonClosed(Balloon* source); + virtual const Balloons& GetActiveBalloons() { + return balloons_; + } + + // MessageLoopForUI::Observer interface. +#if defined(OS_WIN) + virtual void WillProcessMessage(const MSG& event) {} + virtual void DidProcessMessage(const MSG& event); +#endif +#if defined(OS_LINUX) + virtual void WillProcessEvent(GdkEvent* event) {} + virtual void DidProcessEvent(GdkEvent* event); +#endif + + protected: + // Calculates layout values for the balloons including + // the scaling, the max/min sizes, and the upper left corner of each. + class Layout { + public: + Layout(); + + // Refresh the work area and balloon placement. + void OnDisplaySettingsChanged(); + + // TODO(johnnyg): Scale the size to account for the system font factor. + static int min_balloon_width() { return kBalloonMinWidth; } + static int max_balloon_width() { return kBalloonMaxWidth; } + static int min_balloon_height() { return kBalloonMinHeight; } + static int max_balloon_height() { return kBalloonMaxHeight; } + + // Utility function constrains the input rectangle to the min and max sizes. + static gfx::Size ConstrainToSizeLimits(const gfx::Size& rect); + + // Returns both the total space available and the maximum + // allowed per balloon. + // + // The size may be a height or length depending on the way that + // balloons are laid out. + void GetMaxLinearSize(int* max_balloon_size, int* total_size) const; + + // Refresh the cached values for work area and drawing metrics. + // The application should call this method to re-acquire metrics after + // any resolution or settings change. + // Returns true if and only if a metric changed. + bool RefreshSystemMetrics(); + + // Returns the origin for the sequence of balloons depending on layout. + // Should not be used to place a balloon -- only to call NextPosition(). + gfx::Point GetLayoutOrigin() const; + + // Compute the position for the next balloon. + // Start with *position_iterator = GetLayoutOrigin() and call repeatedly + // to get a sequence of positions. Return value is the upper-left coordinate + // for each next balloon. + gfx::Point NextPosition(const gfx::Size& balloon_size, + gfx::Point* position_iterator) const; + + // Return a offscreen location which is offscreen for this layout, + // to be used as the initial position for an animation into view. + gfx::Point OffScreenLocation() const; + + private: + enum Placement { + VERTICALLY_FROM_TOP_RIGHT, + VERTICALLY_FROM_BOTTOM_RIGHT + }; + + // Layout parameters + int VerticalEdgeMargin() const; + int HorizontalEdgeMargin() const; + int InterBalloonMargin() const; + + // Minimum and maximum size of balloon content. + static const int kBalloonMinWidth = 300; + static const int kBalloonMaxWidth = 300; + static const int kBalloonMinHeight = 24; + static const int kBalloonMaxHeight = 120; + + static Placement placement_; + gfx::Rect work_area_; + DISALLOW_COPY_AND_ASSIGN(Layout); + }; + + // Creates a new balloon. Overridable by unit tests. The caller is + // responsible for freeing the pointer returned. + virtual Balloon* MakeBalloon(const Notification& notification, + Profile* profile); + + private: + // The number of balloons being displayed. + int count() const { return balloons_.size(); } + + // Adjusts the positions of the balloons (e.g., when one is closed). + void PositionBalloons(bool is_reposition); + +#if defined(OS_MACOSX) + // Get the work area on Mac OS, without inverting the coordinates. + static gfx::Rect GetMacWorkArea(); +#endif + +#if USE_OFFSETS + // Start and stop observing all UI events. + void AddMessageLoopObserver(); + void RemoveMessageLoopObserver(); + + // Cancel all offset and reposition the balloons normally. + void CancelOffsets(); + + // Handles a mouse motion while the balloons are temporarily offset. + void HandleMouseMoveEvent(); + + // Is the current cursor in the balloon area? + bool IsCursorInBalloonCollection() const; +#endif + + // Queue of active balloons. + typedef std::deque<Balloon*> Balloons; + Balloons balloons_; + + // The layout parameters for balloons in this collection. + Layout layout_; + +#if USE_OFFSETS + // Factory for generating delayed reposition tasks on mouse motion. + ScopedRunnableMethodFactory<BalloonCollectionImpl> reposition_factory_; + + // Is the balloon collection currently listening for UI events? + bool added_as_message_loop_observer_; +#endif + + DISALLOW_COPY_AND_ASSIGN(BalloonCollectionImpl); +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_BALLOON_COLLECTION_IMPL_H_ diff --git a/chrome/browser/notifications/balloon_collection_linux.cc b/chrome/browser/notifications/balloon_collection_linux.cc new file mode 100644 index 0000000..a499292 --- /dev/null +++ b/chrome/browser/notifications/balloon_collection_linux.cc @@ -0,0 +1,68 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/balloon_collection_impl.h" + +#include "chrome/browser/notifications/balloon.h" +#include "chrome/browser/views/notifications/balloon_view.h" +#include "gfx/size.h" + +Balloon* BalloonCollectionImpl::MakeBalloon(const Notification& notification, + Profile* profile) { + Balloon* balloon = new Balloon(notification, profile, this); + + balloon->set_view(new BalloonViewImpl(this)); + gfx::Size size(layout_.min_balloon_width(), layout_.min_balloon_height()); + balloon->set_content_size(size); + return balloon; +} + +int BalloonCollectionImpl::Layout::InterBalloonMargin() const { + return 5; +} + +int BalloonCollectionImpl::Layout::HorizontalEdgeMargin() const { + return 5; +} + +int BalloonCollectionImpl::Layout::VerticalEdgeMargin() const { + return 5; +} + +void BalloonCollectionImpl::DidProcessEvent(GdkEvent* event) { + switch (event->type) { + case GDK_MOTION_NOTIFY: + case GDK_LEAVE_NOTIFY: + HandleMouseMoveEvent(); + break; + default: + break; + } +} + +bool BalloonCollectionImpl::IsCursorInBalloonCollection() const { + if (balloons_.empty()) + return false; + + gfx::Point upper_left = balloons_[balloons_.size() - 1]->GetPosition(); + gfx::Point lower_right = layout_.GetLayoutOrigin(); + + gfx::Rect bounds = gfx::Rect(upper_left.x(), + upper_left.y(), + lower_right.x() - upper_left.x(), + lower_right.y() - upper_left.y()); + + GdkScreen* screen = gdk_screen_get_default(); + GdkDisplay* display = gdk_screen_get_display(screen); + gint x, y; + gdk_display_get_pointer(display, NULL, &x, &y, NULL); + gfx::Point cursor(x, y); + + return bounds.Contains(cursor); +} + +// static +BalloonCollection* BalloonCollection::Create() { + return new BalloonCollectionImpl(); +} diff --git a/chrome/browser/notifications/balloon_collection_mac.mm b/chrome/browser/notifications/balloon_collection_mac.mm new file mode 100644 index 0000000..dc4172d --- /dev/null +++ b/chrome/browser/notifications/balloon_collection_mac.mm @@ -0,0 +1,39 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/balloon_collection_impl.h" + +#include "chrome/browser/cocoa/notifications/balloon_view_bridge.h" + +Balloon* BalloonCollectionImpl::MakeBalloon(const Notification& notification, + Profile* profile) { + Balloon* balloon = new Balloon(notification, profile, this); + balloon->set_view(new BalloonViewBridge()); + gfx::Size size(layout_.min_balloon_width(), layout_.min_balloon_height()); + balloon->set_content_size(size); + return balloon; +} + +// static +gfx::Rect BalloonCollectionImpl::GetMacWorkArea() { + NSScreen* primary = [[NSScreen screens] objectAtIndex:0]; + return gfx::Rect(NSRectToCGRect([primary visibleFrame])); +} + +int BalloonCollectionImpl::Layout::InterBalloonMargin() const { + return 5; +} + +int BalloonCollectionImpl::Layout::HorizontalEdgeMargin() const { + return 5; +} + +int BalloonCollectionImpl::Layout::VerticalEdgeMargin() const { + return 5; +} + +// static +BalloonCollection* BalloonCollection::Create() { + return new BalloonCollectionImpl(); +} diff --git a/chrome/browser/notifications/balloon_collection_win.cc b/chrome/browser/notifications/balloon_collection_win.cc new file mode 100644 index 0000000..b5c2351 --- /dev/null +++ b/chrome/browser/notifications/balloon_collection_win.cc @@ -0,0 +1,63 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/balloon_collection_impl.h" + +#include "chrome/browser/notifications/balloon.h" +#include "chrome/browser/views/notifications/balloon_view.h" +#include "gfx/rect.h" + +Balloon* BalloonCollectionImpl::MakeBalloon(const Notification& notification, + Profile* profile) { + Balloon* balloon = new Balloon(notification, profile, this); + balloon->set_view(new BalloonViewImpl(this)); + gfx::Size size(layout_.min_balloon_width(), layout_.min_balloon_height()); + balloon->set_content_size(size); + return balloon; +} + +int BalloonCollectionImpl::Layout::InterBalloonMargin() const { + return 3; +} + +int BalloonCollectionImpl::Layout::HorizontalEdgeMargin() const { + return 2; +} + +int BalloonCollectionImpl::Layout::VerticalEdgeMargin() const { + return 0; +} + +void BalloonCollectionImpl::DidProcessMessage(const MSG& msg) { + switch (msg.message) { + case WM_MOUSEMOVE: + case WM_MOUSELEAVE: + case WM_NCMOUSELEAVE: + HandleMouseMoveEvent(); + break; + } +} + +bool BalloonCollectionImpl::IsCursorInBalloonCollection() const { + if (balloons_.empty()) + return false; + + gfx::Point upper_left = balloons_[balloons_.size() - 1]->GetPosition(); + gfx::Point lower_right = layout_.GetLayoutOrigin(); + + gfx::Rect bounds = gfx::Rect(upper_left.x(), + upper_left.y(), + lower_right.x() - upper_left.x(), + lower_right.y() - upper_left.y()); + + DWORD pos = GetMessagePos(); + gfx::Point cursor(pos); + + return bounds.Contains(cursor); +} + +// static +BalloonCollection* BalloonCollection::Create() { + return new BalloonCollectionImpl(); +} diff --git a/chrome/browser/notifications/balloon_host.cc b/chrome/browser/notifications/balloon_host.cc new file mode 100644 index 0000000..e962f5a --- /dev/null +++ b/chrome/browser/notifications/balloon_host.cc @@ -0,0 +1,171 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/balloon_host.h" + +#include "chrome/browser/browser_list.h" +#include "chrome/browser/in_process_webkit/webkit_context.h" +#include "chrome/browser/extensions/extension_process_manager.h" +#include "chrome/browser/notifications/balloon.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/renderer_host/render_view_host.h" +#include "chrome/browser/renderer_host/site_instance.h" +#include "chrome/browser/renderer_preferences_util.h" +#include "chrome/common/bindings_policy.h" +#include "chrome/common/notification_service.h" +#include "chrome/common/notification_type.h" +#include "chrome/common/render_messages.h" +#include "chrome/common/renderer_preferences.h" +#include "chrome/common/url_constants.h" + +BalloonHost::BalloonHost(Balloon* balloon) + : render_view_host_(NULL), + balloon_(balloon), + initialized_(false), + should_notify_on_disconnect_(false) { + DCHECK(balloon_); + + // If the notification is for an extension URL, make sure to use the extension + // process to render it, so that it can communicate with other views in the + // extension. + const GURL& balloon_url = balloon_->notification().content_url(); + if (balloon_url.SchemeIs(chrome::kExtensionScheme)) { + site_instance_ = + balloon_->profile()->GetExtensionProcessManager()->GetSiteInstanceForURL( + balloon_url); + } else { + site_instance_ = SiteInstance::CreateSiteInstance(balloon_->profile()); + } +} + +void BalloonHost::Shutdown() { + if (render_view_host_) { + render_view_host_->Shutdown(); + render_view_host_ = NULL; + } +} + +WebPreferences BalloonHost::GetWebkitPrefs() { + WebPreferences prefs; + prefs.allow_scripts_to_close_windows = true; + return prefs; +} + +void BalloonHost::Close(RenderViewHost* render_view_host) { + balloon_->CloseByScript(); + NotifyDisconnect(); +} + +void BalloonHost::RenderViewCreated(RenderViewHost* render_view_host) { + render_view_host->Send(new ViewMsg_DisableScrollbarsForSmallWindows( + render_view_host->routing_id(), balloon_->min_scrollbar_size())); + render_view_host->WasResized(); + render_view_host->EnablePreferredSizeChangedMode( + kPreferredSizeWidth | kPreferredSizeHeightThisIsSlow); +} + +void BalloonHost::RenderViewReady(RenderViewHost* render_view_host) { + should_notify_on_disconnect_ = true; + NotificationService::current()->Notify( + NotificationType::NOTIFY_BALLOON_CONNECTED, + Source<BalloonHost>(this), NotificationService::NoDetails()); +} + +void BalloonHost::RenderViewGone(RenderViewHost* render_view_host) { + Close(render_view_host); +} + +void BalloonHost::ProcessDOMUIMessage(const std::string& message, + const ListValue* content, + const GURL& source_url, + int request_id, + bool has_callback) { + if (extension_function_dispatcher_.get()) { + extension_function_dispatcher_->HandleRequest( + message, content, source_url, request_id, has_callback); + } +} + +// RenderViewHostDelegate::View methods implemented to allow links to +// open pages in new tabs. +void BalloonHost::CreateNewWindow( + int route_id, + WindowContainerType window_container_type, + const string16& frame_name) { + delegate_view_helper_.CreateNewWindow( + route_id, + balloon_->profile(), + site_instance_.get(), + DOMUIFactory::GetDOMUIType(balloon_->notification().content_url()), + this, + window_container_type, + frame_name); +} + +void BalloonHost::ShowCreatedWindow(int route_id, + WindowOpenDisposition disposition, + const gfx::Rect& initial_pos, + bool user_gesture) { + // Don't allow pop-ups from notifications. + if (disposition == NEW_POPUP) + return; + + TabContents* contents = delegate_view_helper_.GetCreatedWindow(route_id); + if (!contents) + return; + Browser* browser = BrowserList::GetLastActiveWithProfile(balloon_->profile()); + if (!browser) + return; + + browser->AddTabContents(contents, disposition, initial_pos, user_gesture); +} + +void BalloonHost::UpdatePreferredSize(const gfx::Size& new_size) { + balloon_->SetContentPreferredSize(new_size); +} + +RendererPreferences BalloonHost::GetRendererPrefs(Profile* profile) const { + RendererPreferences preferences; + renderer_preferences_util::UpdateFromSystemSettings(&preferences, profile); + return preferences; +} + +void BalloonHost::Init() { + DCHECK(!render_view_host_) << "BalloonViewHost already initialized."; + int64 session_storage_namespace_id = balloon_->profile()->GetWebKitContext()-> + dom_storage_context()->AllocateSessionStorageNamespaceId(); + RenderViewHost* rvh = new RenderViewHost(site_instance_.get(), + this, MSG_ROUTING_NONE, + session_storage_namespace_id); + if (GetProfile()->GetExtensionsService()) { + extension_function_dispatcher_.reset( + ExtensionFunctionDispatcher::Create( + rvh, this, balloon_->notification().content_url())); + } + if (extension_function_dispatcher_.get()) { + rvh->AllowBindings(BindingsPolicy::EXTENSION); + rvh->set_is_extension_process(true); + } + + // Do platform-specific initialization. + render_view_host_ = rvh; + InitRenderWidgetHostView(); + DCHECK(render_widget_host_view()); + + rvh->set_view(render_widget_host_view()); + rvh->CreateRenderView(GetProfile()->GetRequestContext(), string16()); + rvh->NavigateToURL(balloon_->notification().content_url()); + + initialized_ = true; +} + +void BalloonHost::NotifyDisconnect() { + if (!should_notify_on_disconnect_) + return; + + should_notify_on_disconnect_ = false; + NotificationService::current()->Notify( + NotificationType::NOTIFY_BALLOON_DISCONNECTED, + Source<BalloonHost>(this), NotificationService::NoDetails()); +} diff --git a/chrome/browser/notifications/balloon_host.h b/chrome/browser/notifications/balloon_host.h new file mode 100644 index 0000000..07ddf3d --- /dev/null +++ b/chrome/browser/notifications/balloon_host.h @@ -0,0 +1,150 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_NOTIFICATIONS_BALLOON_HOST_H_ +#define CHROME_BROWSER_NOTIFICATIONS_BALLOON_HOST_H_ + +#include <string> + +#include "chrome/browser/extensions/extension_function_dispatcher.h" +#include "chrome/browser/notifications/balloon.h" +#include "chrome/browser/notifications/notification.h" +#include "chrome/browser/renderer_host/render_view_host_delegate.h" +#include "chrome/browser/renderer_host/site_instance.h" +#include "chrome/browser/tab_contents/render_view_host_delegate_helper.h" +#include "chrome/common/extensions/extension_constants.h" +#include "chrome/common/renderer_preferences.h" +#include "webkit/glue/webpreferences.h" + +class Browser; +class Profile; + +class BalloonHost : public RenderViewHostDelegate, + public RenderViewHostDelegate::View, + public ExtensionFunctionDispatcher::Delegate { + public: + explicit BalloonHost(Balloon* balloon); + + // Initialize the view. + void Init(); + + // Stops showing the balloon. + void Shutdown(); + + // ExtensionFunctionDispatcher::Delegate overrides. + virtual Browser* GetBrowser() const { + // Notifications aren't associated with a particular browser. + return NULL; + } + virtual gfx::NativeView GetNativeViewOfHost() { + // TODO(aa): Should this return the native view of the BalloonView*? + return NULL; + } + virtual TabContents* associated_tab_contents() { return NULL; } + + RenderViewHost* render_view_host() const { return render_view_host_; } + + std::wstring GetSource() const { + return balloon_->notification().display_source(); + } + + // RenderViewHostDelegate overrides. + virtual WebPreferences GetWebkitPrefs(); + virtual SiteInstance* GetSiteInstance() const { + return site_instance_.get(); + } + virtual Profile* GetProfile() const { return balloon_->profile(); } + virtual const GURL& GetURL() const { + return balloon_->notification().content_url(); + } + virtual void Close(RenderViewHost* render_view_host); + virtual void RenderViewCreated(RenderViewHost* render_view_host); + virtual void RenderViewReady(RenderViewHost* render_view_host); + virtual void RenderViewGone(RenderViewHost* render_view_host); + virtual void UpdateTitle(RenderViewHost* render_view_host, + int32 page_id, const std::wstring& title) {} + virtual int GetBrowserWindowID() const { + return extension_misc::kUnknownWindowId; + } + virtual ViewType::Type GetRenderViewType() const { + return ViewType::NOTIFICATION; + } + virtual RenderViewHostDelegate::View* GetViewDelegate() { + return this; + } + virtual void ProcessDOMUIMessage(const std::string& message, + const ListValue* content, + const GURL& source_url, + int request_id, + bool has_callback); + + // RenderViewHostDelegate::View methods. Only the ones for opening new + // windows are currently implemented. + virtual void CreateNewWindow( + int route_id, + WindowContainerType window_container_type, + const string16& frame_name); + virtual void CreateNewWidget(int route_id, WebKit::WebPopupType popup_type) {} + virtual void ShowCreatedWindow(int route_id, + WindowOpenDisposition disposition, + const gfx::Rect& initial_pos, + bool user_gesture); + virtual void ShowCreatedWidget(int route_id, + const gfx::Rect& initial_pos) {} + virtual void ShowContextMenu(const ContextMenuParams& params) {} + virtual void StartDragging(const WebDropData& drop_data, + WebKit::WebDragOperationsMask allowed_ops) {} + virtual void StartDragging(const WebDropData&, + WebKit::WebDragOperationsMask, + const SkBitmap&, + const gfx::Point&) {} + virtual void UpdateDragCursor(WebKit::WebDragOperation operation) {} + virtual void GotFocus() {} + virtual void TakeFocus(bool reverse) {} + virtual bool PreHandleKeyboardEvent(const NativeWebKeyboardEvent& event, + bool* is_keyboard_shortcut) { + return false; + } + virtual void HandleKeyboardEvent(const NativeWebKeyboardEvent& event) {} + virtual void HandleMouseEvent() {} + virtual void HandleMouseLeave() {} + virtual void UpdatePreferredSize(const gfx::Size& pref_size); + virtual RendererPreferences GetRendererPrefs(Profile* profile) const; + + protected: + // Must override in platform specific implementations. + virtual void InitRenderWidgetHostView() = 0; + virtual RenderWidgetHostView* render_widget_host_view() const = 0; + + // Owned pointer to the host for the renderer process. + RenderViewHost* render_view_host_; + + private: + // Called to send an event that the balloon has been disconnected from + // a renderer (if should_notify_on_disconnect_ is true). + void NotifyDisconnect(); + + // Non-owned pointer to the associated balloon. + Balloon* balloon_; + + // True after Init() has completed. + bool initialized_; + + // Indicates whether we should notify about disconnection of this balloon. + // This is used to ensure disconnection notifications only happen if + // a connection notification has happened and that they happen only once. + bool should_notify_on_disconnect_; + + // Site instance for the balloon/profile, to be used for opening new links. + scoped_refptr<SiteInstance> site_instance_; + + // Common implementations of some RenderViewHostDelegate::View methods. + RenderViewHostDelegateViewHelper delegate_view_helper_; + + // Handles requests to extension APIs. Will only be non-NULL if we are + // rendering a page from an extension. + scoped_ptr<ExtensionFunctionDispatcher> extension_function_dispatcher_; +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_BALLOON_HOST_H_ diff --git a/chrome/browser/notifications/desktop_notification_service.cc b/chrome/browser/notifications/desktop_notification_service.cc new file mode 100644 index 0000000..bc66077 --- /dev/null +++ b/chrome/browser/notifications/desktop_notification_service.cc @@ -0,0 +1,582 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/desktop_notification_service.h" + +#include "app/l10n_util.h" +#include "app/resource_bundle.h" +#include "base/thread.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/browser_child_process_host.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/extensions/extensions_service.h" +#include "chrome/browser/notifications/notification.h" +#include "chrome/browser/notifications/notification_object_proxy.h" +#include "chrome/browser/notifications/notification_ui_manager.h" +#include "chrome/browser/notifications/notifications_prefs_cache.h" +#include "chrome/browser/pref_service.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/renderer_host/render_process_host.h" +#include "chrome/browser/renderer_host/render_view_host.h" +#include "chrome/browser/renderer_host/site_instance.h" +#include "chrome/browser/scoped_pref_update.h" +#include "chrome/browser/tab_contents/infobar_delegate.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/worker_host/worker_process_host.h" +#include "chrome/common/notification_service.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/render_messages.h" +#include "chrome/common/url_constants.h" +#include "grit/browser_resources.h" +#include "grit/chromium_strings.h" +#include "grit/generated_resources.h" +#include "grit/theme_resources.h" +#include "net/base/escape.h" +#include "third_party/WebKit/WebKit/chromium/public/WebNotificationPresenter.h" + +using WebKit::WebNotificationPresenter; +using WebKit::WebTextDirection; + +const ContentSetting kDefaultSetting = CONTENT_SETTING_ASK; + +// static +string16 DesktopNotificationService::CreateDataUrl( + const GURL& icon_url, const string16& title, const string16& body, + WebTextDirection dir) { + int resource; + string16 line_name; + string16 line; + std::vector<std::string> subst; + if (icon_url.is_valid()) { + resource = IDR_NOTIFICATION_ICON_HTML; + subst.push_back(icon_url.spec()); + subst.push_back(EscapeForHTML(UTF16ToUTF8(title))); + subst.push_back(EscapeForHTML(UTF16ToUTF8(body))); + // icon float position + subst.push_back(dir == WebKit::WebTextDirectionRightToLeft ? + "right" : "left"); + } else if (title.empty() || body.empty()) { + resource = IDR_NOTIFICATION_1LINE_HTML; + line = title.empty() ? body : title; + // Strings are div names in the template file. + line_name = title.empty() ? ASCIIToUTF16("description") + : ASCIIToUTF16("title"); + subst.push_back(EscapeForHTML(UTF16ToUTF8(line_name))); + subst.push_back(EscapeForHTML(UTF16ToUTF8(line))); + } else { + resource = IDR_NOTIFICATION_2LINE_HTML; + subst.push_back(EscapeForHTML(UTF16ToUTF8(title))); + subst.push_back(EscapeForHTML(UTF16ToUTF8(body))); + } + // body text direction + subst.push_back(dir == WebKit::WebTextDirectionRightToLeft ? + "rtl" : "ltr"); + + const base::StringPiece template_html( + ResourceBundle::GetSharedInstance().GetRawDataResource( + resource)); + + if (template_html.empty()) { + NOTREACHED() << "unable to load template. ID: " << resource; + return string16(); + } + + std::string data = ReplaceStringPlaceholders(template_html, subst, NULL); + return UTF8ToUTF16("data:text/html;charset=utf-8," + + EscapeQueryParamValue(data, false)); +} + +// A task object which calls the renderer to inform the web page that the +// permission request has completed. +class NotificationPermissionCallbackTask : public Task { + public: + NotificationPermissionCallbackTask(int process_id, int route_id, + int request_id) + : process_id_(process_id), + route_id_(route_id), + request_id_(request_id) { + } + + virtual void Run() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO)); + RenderViewHost* host = RenderViewHost::FromID(process_id_, route_id_); + if (host) + host->Send(new ViewMsg_PermissionRequestDone(route_id_, request_id_)); + } + + private: + int process_id_; + int route_id_; + int request_id_; +}; + +// The delegate for the infobar shown when an origin requests notification +// permissions. +class NotificationPermissionInfoBarDelegate : public ConfirmInfoBarDelegate { + public: + NotificationPermissionInfoBarDelegate(TabContents* contents, + const GURL& origin, + const std::wstring& display_name, + int process_id, + int route_id, + int callback_context) + : ConfirmInfoBarDelegate(contents), + origin_(origin), + display_name_(display_name), + profile_(contents->profile()), + process_id_(process_id), + route_id_(route_id), + callback_context_(callback_context), + action_taken_(false) { + } + + // Overridden from ConfirmInfoBarDelegate: + virtual void InfoBarClosed() { + if (!action_taken_) + UMA_HISTOGRAM_COUNTS("NotificationPermissionRequest.Ignored", 1); + + ChromeThread::PostTask( + ChromeThread::IO, FROM_HERE, + new NotificationPermissionCallbackTask( + process_id_, route_id_, callback_context_)); + + delete this; + } + + virtual std::wstring GetMessageText() const { + return l10n_util::GetStringF(IDS_NOTIFICATION_PERMISSIONS, display_name_); + } + + virtual SkBitmap* GetIcon() const { + return ResourceBundle::GetSharedInstance().GetBitmapNamed( + IDR_PRODUCT_ICON_32); + } + + virtual int GetButtons() const { + return BUTTON_OK | BUTTON_CANCEL | BUTTON_OK_DEFAULT; + } + + virtual std::wstring GetButtonLabel(InfoBarButton button) const { + return button == BUTTON_OK ? + l10n_util::GetString(IDS_NOTIFICATION_PERMISSION_YES) : + l10n_util::GetString(IDS_NOTIFICATION_PERMISSION_NO); + } + + virtual bool Accept() { + UMA_HISTOGRAM_COUNTS("NotificationPermissionRequest.Allowed", 1); + profile_->GetDesktopNotificationService()->GrantPermission(origin_); + action_taken_ = true; + return true; + } + + virtual bool Cancel() { + UMA_HISTOGRAM_COUNTS("NotificationPermissionRequest.Denied", 1); + profile_->GetDesktopNotificationService()->DenyPermission(origin_); + action_taken_ = true; + return true; + } + + private: + // The origin we are asking for permissions on. + GURL origin_; + + // The display name for the origin to be displayed. Will be different from + // origin_ for extensions. + std::wstring display_name_; + + // The Profile that we restore sessions from. + Profile* profile_; + + // The callback information that tells us how to respond to javascript via + // the correct RenderView. + int process_id_; + int route_id_; + int callback_context_; + + // Whether the user clicked one of the buttons. + bool action_taken_; + + DISALLOW_COPY_AND_ASSIGN(NotificationPermissionInfoBarDelegate); +}; + +DesktopNotificationService::DesktopNotificationService(Profile* profile, + NotificationUIManager* ui_manager) + : profile_(profile), + ui_manager_(ui_manager) { + InitPrefs(); + StartObserving(); +} + +DesktopNotificationService::~DesktopNotificationService() { + StopObserving(); +} + +void DesktopNotificationService::RegisterUserPrefs(PrefService* user_prefs) { + if (!user_prefs->FindPreference( + prefs::kDesktopNotificationDefaultContentSetting)) { + user_prefs->RegisterIntegerPref( + prefs::kDesktopNotificationDefaultContentSetting, kDefaultSetting); + } + if (!user_prefs->FindPreference(prefs::kDesktopNotificationAllowedOrigins)) + user_prefs->RegisterListPref(prefs::kDesktopNotificationAllowedOrigins); + if (!user_prefs->FindPreference(prefs::kDesktopNotificationDeniedOrigins)) + user_prefs->RegisterListPref(prefs::kDesktopNotificationDeniedOrigins); +} + +// Initialize the cache with the allowed and denied origins, or +// create the preferences if they don't exist yet. +void DesktopNotificationService::InitPrefs() { + PrefService* prefs = profile_->GetPrefs(); + std::vector<GURL> allowed_origins; + std::vector<GURL> denied_origins; + ContentSetting default_content_setting = CONTENT_SETTING_DEFAULT; + + if (!profile_->IsOffTheRecord()) { + default_content_setting = IntToContentSetting( + prefs->GetInteger(prefs::kDesktopNotificationDefaultContentSetting)); + allowed_origins = GetAllowedOrigins(); + denied_origins = GetBlockedOrigins(); + } + + prefs_cache_ = new NotificationsPrefsCache(); + prefs_cache_->SetCacheDefaultContentSetting(default_content_setting); + prefs_cache_->SetCacheAllowedOrigins(allowed_origins); + prefs_cache_->SetCacheDeniedOrigins(denied_origins); + prefs_cache_->set_is_initialized(true); +} + +void DesktopNotificationService::StartObserving() { + if (!profile_->IsOffTheRecord()) { + PrefService* prefs = profile_->GetPrefs(); + prefs->AddPrefObserver(prefs::kDesktopNotificationDefaultContentSetting, + this); + prefs->AddPrefObserver(prefs::kDesktopNotificationAllowedOrigins, this); + prefs->AddPrefObserver(prefs::kDesktopNotificationDeniedOrigins, this); + } +} + +void DesktopNotificationService::StopObserving() { + if (!profile_->IsOffTheRecord()) { + PrefService* prefs = profile_->GetPrefs(); + prefs->RemovePrefObserver(prefs::kDesktopNotificationDefaultContentSetting, + this); + prefs->RemovePrefObserver(prefs::kDesktopNotificationAllowedOrigins, this); + prefs->RemovePrefObserver(prefs::kDesktopNotificationDeniedOrigins, this); + } +} + +void DesktopNotificationService::GrantPermission(const GURL& origin) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + PersistPermissionChange(origin, true); + + // Schedule a cache update on the IO thread. + ChromeThread::PostTask( + ChromeThread::IO, FROM_HERE, + NewRunnableMethod( + prefs_cache_.get(), &NotificationsPrefsCache::CacheAllowedOrigin, + origin)); +} + +void DesktopNotificationService::DenyPermission(const GURL& origin) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + PersistPermissionChange(origin, false); + + // Schedule a cache update on the IO thread. + ChromeThread::PostTask( + ChromeThread::IO, FROM_HERE, + NewRunnableMethod( + prefs_cache_.get(), &NotificationsPrefsCache::CacheDeniedOrigin, + origin)); +} + +void DesktopNotificationService::Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + DCHECK(NotificationType::PREF_CHANGED == type); + PrefService* prefs = profile_->GetPrefs(); + std::wstring* name = Details<std::wstring>(details).ptr(); + + if (0 == name->compare(prefs::kDesktopNotificationAllowedOrigins)) { + std::vector<GURL> allowed_origins(GetAllowedOrigins()); + // Schedule a cache update on the IO thread. + ChromeThread::PostTask( + ChromeThread::IO, FROM_HERE, + NewRunnableMethod( + prefs_cache_.get(), + &NotificationsPrefsCache::SetCacheAllowedOrigins, + allowed_origins)); + } else if (0 == name->compare(prefs::kDesktopNotificationDeniedOrigins)) { + std::vector<GURL> denied_origins(GetBlockedOrigins()); + // Schedule a cache update on the IO thread. + ChromeThread::PostTask( + ChromeThread::IO, FROM_HERE, + NewRunnableMethod( + prefs_cache_.get(), + &NotificationsPrefsCache::SetCacheDeniedOrigins, + denied_origins)); + } else if (0 == name->compare( + prefs::kDesktopNotificationDefaultContentSetting)) { + const ContentSetting default_content_setting = IntToContentSetting( + prefs->GetInteger(prefs::kDesktopNotificationDefaultContentSetting)); + + // Schedule a cache update on the IO thread. + ChromeThread::PostTask( + ChromeThread::IO, FROM_HERE, + NewRunnableMethod( + prefs_cache_.get(), + &NotificationsPrefsCache::SetCacheDefaultContentSetting, + default_content_setting)); + } +} + +void DesktopNotificationService::PersistPermissionChange( + const GURL& origin, bool is_allowed) { + // Don't persist changes when off the record. + if (profile_->IsOffTheRecord()) + return; + + PrefService* prefs = profile_->GetPrefs(); + + // |Observe()| updates the whole permission set in the cache, but only a + // single origin has changed. Hence, callers of this method manually + // schedule a task to update the prefs cache, and the prefs observer is + // disabled while the update runs. + StopObserving(); + + bool allowed_changed = false; + bool denied_changed = false; + + ListValue* allowed_sites = + prefs->GetMutableList(prefs::kDesktopNotificationAllowedOrigins); + ListValue* denied_sites = + prefs->GetMutableList(prefs::kDesktopNotificationDeniedOrigins); + { + // value is passed to the preferences list, or deleted. + StringValue* value = new StringValue(origin.spec()); + + // Remove from one list and add to the other. + if (is_allowed) { + // Remove from the denied list. + if (denied_sites->Remove(*value) != -1) + denied_changed = true; + + // Add to the allowed list. + if (allowed_sites->AppendIfNotPresent(value)) + allowed_changed = true; + else + delete value; + } else { + // Remove from the allowed list. + if (allowed_sites->Remove(*value) != -1) + allowed_changed = true; + + // Add to the denied list. + if (denied_sites->AppendIfNotPresent(value)) + denied_changed = true; + else + delete value; + } + } + + // Persist the pref if anthing changed, but only send updates for the + // list that changed. + if (allowed_changed || denied_changed) { + if (allowed_changed) { + ScopedPrefUpdate update_allowed( + prefs, prefs::kDesktopNotificationAllowedOrigins); + } + if (denied_changed) { + ScopedPrefUpdate updateDenied( + prefs, prefs::kDesktopNotificationDeniedOrigins); + } + prefs->ScheduleSavePersistentPrefs(); + } + StartObserving(); +} + +ContentSetting DesktopNotificationService::GetDefaultContentSetting() { + PrefService* prefs = profile_->GetPrefs(); + ContentSetting setting = IntToContentSetting( + prefs->GetInteger(prefs::kDesktopNotificationDefaultContentSetting)); + if (setting == CONTENT_SETTING_DEFAULT) + setting = kDefaultSetting; + return setting; +} + +void DesktopNotificationService::SetDefaultContentSetting( + ContentSetting setting) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + profile_->GetPrefs()->SetInteger( + prefs::kDesktopNotificationDefaultContentSetting, + setting == CONTENT_SETTING_DEFAULT ? kDefaultSetting : setting); + // The cache is updated through the notification observer. +} + +std::vector<GURL> DesktopNotificationService::GetAllowedOrigins() { + std::vector<GURL> allowed_origins; + PrefService* prefs = profile_->GetPrefs(); + const ListValue* allowed_sites = + prefs->GetList(prefs::kDesktopNotificationAllowedOrigins); + if (allowed_sites) { + NotificationsPrefsCache::ListValueToGurlVector(*allowed_sites, + &allowed_origins); + } + return allowed_origins; +} + +std::vector<GURL> DesktopNotificationService::GetBlockedOrigins() { + std::vector<GURL> denied_origins; + PrefService* prefs = profile_->GetPrefs(); + const ListValue* denied_sites = + prefs->GetList(prefs::kDesktopNotificationDeniedOrigins); + if (denied_sites) { + NotificationsPrefsCache::ListValueToGurlVector(*denied_sites, + &denied_origins); + } + return denied_origins; +} + +void DesktopNotificationService::ResetAllowedOrigin(const GURL& origin) { + if (profile_->IsOffTheRecord()) + return; + + // Since this isn't called often, let the normal observer behavior update the + // cache in this case. + PrefService* prefs = profile_->GetPrefs(); + ListValue* allowed_sites = + prefs->GetMutableList(prefs::kDesktopNotificationAllowedOrigins); + { + StringValue value(origin.spec()); + int removed_index = allowed_sites->Remove(value); + DCHECK_NE(-1, removed_index) << origin << " was not allowed"; + ScopedPrefUpdate update_allowed( + prefs, prefs::kDesktopNotificationAllowedOrigins); + } + prefs->ScheduleSavePersistentPrefs(); +} + +void DesktopNotificationService::ResetBlockedOrigin(const GURL& origin) { + if (profile_->IsOffTheRecord()) + return; + + // Since this isn't called often, let the normal observer behavior update the + // cache in this case. + PrefService* prefs = profile_->GetPrefs(); + ListValue* denied_sites = + prefs->GetMutableList(prefs::kDesktopNotificationDeniedOrigins); + { + StringValue value(origin.spec()); + int removed_index = denied_sites->Remove(value); + DCHECK_NE(-1, removed_index) << origin << " was not blocked"; + ScopedPrefUpdate update_allowed( + prefs, prefs::kDesktopNotificationDeniedOrigins); + } + prefs->ScheduleSavePersistentPrefs(); +} + +void DesktopNotificationService::ResetAllOrigins() { + PrefService* prefs = profile_->GetPrefs(); + prefs->ClearPref(prefs::kDesktopNotificationAllowedOrigins); + prefs->ClearPref(prefs::kDesktopNotificationDeniedOrigins); +} + +ContentSetting DesktopNotificationService::GetContentSetting( + const GURL& origin) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + if (profile_->IsOffTheRecord()) + return kDefaultSetting; + + std::vector<GURL> allowed_origins(GetAllowedOrigins()); + if (std::find(allowed_origins.begin(), allowed_origins.end(), origin) != + allowed_origins.end()) + return CONTENT_SETTING_ALLOW; + + std::vector<GURL> denied_origins(GetBlockedOrigins()); + if (std::find(denied_origins.begin(), denied_origins.end(), origin) != + denied_origins.end()) + return CONTENT_SETTING_BLOCK; + + return GetDefaultContentSetting(); +} + +void DesktopNotificationService::RequestPermission( + const GURL& origin, int process_id, int route_id, int callback_context, + TabContents* tab) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + if (!tab) + return; + + // If |origin| hasn't been seen before and the default content setting for + // notifications is "ask", show an infobar. + // The cache can only answer queries on the IO thread once it's initialized, + // so don't ask the cache. + ContentSetting setting = GetContentSetting(origin); + if (setting == CONTENT_SETTING_ASK) { + // Show an info bar requesting permission. + std::wstring display_name = DisplayNameForOrigin(origin); + + tab->AddInfoBar(new NotificationPermissionInfoBarDelegate( + tab, origin, display_name, process_id, route_id, callback_context)); + } else { + // Notify renderer immediately. + ChromeThread::PostTask( + ChromeThread::IO, FROM_HERE, + new NotificationPermissionCallbackTask( + process_id, route_id, callback_context)); + } +} + +void DesktopNotificationService::ShowNotification( + const Notification& notification) { + ui_manager_->Add(notification, profile_); +} + +bool DesktopNotificationService::CancelDesktopNotification( + int process_id, int route_id, int notification_id) { + scoped_refptr<NotificationObjectProxy> proxy( + new NotificationObjectProxy(process_id, route_id, notification_id, + false)); + // TODO(johnnyg): clean up this "empty" notification. + Notification notif(GURL(), GURL(), L"", ASCIIToUTF16(""), proxy); + return ui_manager_->Cancel(notif); +} + + +bool DesktopNotificationService::ShowDesktopNotification( + const ViewHostMsg_ShowNotification_Params& params, + int process_id, int route_id, DesktopNotificationSource source) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + const GURL& origin = params.origin; + NotificationObjectProxy* proxy = + new NotificationObjectProxy(process_id, route_id, + params.notification_id, + source == WorkerNotification); + GURL contents; + if (params.is_html) { + contents = params.contents_url; + } else { + // "upconvert" the string parameters to a data: URL. + contents = GURL( + CreateDataUrl(params.icon_url, params.title, params.body, + params.direction)); + } + Notification notif( + origin, contents, DisplayNameForOrigin(origin), params.replace_id, proxy); + ShowNotification(notif); + return true; +} + +std::wstring DesktopNotificationService::DisplayNameForOrigin( + const GURL& origin) { + // If the source is an extension, lookup the display name. + if (origin.SchemeIs(chrome::kExtensionScheme)) { + ExtensionsService* ext_service = profile_->GetExtensionsService(); + if (ext_service) { + Extension* extension = ext_service->GetExtensionByURL(origin); + if (extension) + return UTF8ToWide(extension->name()); + } + } + return UTF8ToWide(origin.host()); +} diff --git a/chrome/browser/notifications/desktop_notification_service.h b/chrome/browser/notifications/desktop_notification_service.h new file mode 100644 index 0000000..ae38b18 --- /dev/null +++ b/chrome/browser/notifications/desktop_notification_service.h @@ -0,0 +1,136 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_NOTIFICATIONS_DESKTOP_NOTIFICATION_SERVICE_H_ +#define CHROME_BROWSER_NOTIFICATIONS_DESKTOP_NOTIFICATION_SERVICE_H_ + +#include <set> + +#include "base/basictypes.h" +#include "chrome/browser/notifications/notification.h" +#include "chrome/common/content_settings.h" +#include "chrome/common/notification_observer.h" +#include "chrome/common/notification_registrar.h" +#include "chrome/common/notification_service.h" +#include "googleurl/src/gurl.h" +#include "third_party/WebKit/WebKit/chromium/public/WebTextDirection.h" + +class NotificationUIManager; +class NotificationsPrefsCache; +class PrefService; +class Profile; +class Task; +class TabContents; +struct ViewHostMsg_ShowNotification_Params; + +// The DesktopNotificationService is an object, owned by the Profile, +// which provides the creation of desktop "toasts" to web pages and workers. +class DesktopNotificationService : public NotificationObserver { + public: + enum DesktopNotificationSource { + PageNotification, + WorkerNotification + }; + + DesktopNotificationService(Profile* profile, + NotificationUIManager* ui_manager); + virtual ~DesktopNotificationService(); + + // Requests permission (using an info-bar) for a given origin. + // |callback_context| contains an opaque value to pass back to the + // requesting process when the info-bar finishes. + void RequestPermission(const GURL& origin, + int process_id, + int route_id, + int callback_context, + TabContents* tab); + + // ShowNotification is called on the UI thread handling IPCs from a child + // process, identified by |process_id| and |route_id|. |source| indicates + // whether the script is in a worker or page. |params| contains all the + // other parameters supplied by the worker or page. + bool ShowDesktopNotification( + const ViewHostMsg_ShowNotification_Params& params, + int process_id, int route_id, DesktopNotificationSource source); + + // Cancels a notification. If it has already been shown, it will be + // removed from the screen. If it hasn't been shown yet, it won't be + // shown. + bool CancelDesktopNotification(int process_id, + int route_id, + int notification_id); + + // Methods to setup and modify permission preferences. + void GrantPermission(const GURL& origin); + void DenyPermission(const GURL& origin); + + // NotificationObserver implementation. + virtual void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details); + + NotificationsPrefsCache* prefs_cache() { return prefs_cache_; } + + // Creates a data:xxxx URL which contains the full HTML for a notification + // using supplied icon, title, and text, run through a template which contains + // the standard formatting for notifications. + static string16 CreateDataUrl(const GURL& icon_url, + const string16& title, + const string16& body, + WebKit::WebTextDirection dir); + + // The default content setting determines how to handle origins that haven't + // been allowed or denied yet. + ContentSetting GetDefaultContentSetting(); + void SetDefaultContentSetting(ContentSetting setting); + + // Returns all origins that explicitly have been allowed. + std::vector<GURL> GetAllowedOrigins(); + + // Returns all origins that explicitly have been denied. + std::vector<GURL> GetBlockedOrigins(); + + // Removes an origin from the "explicitly allowed" set. + void ResetAllowedOrigin(const GURL& origin); + + // Removes an origin from the "explicitly denied" set. + void ResetBlockedOrigin(const GURL& origin); + + // Clears the sets of explicitly allowed and denied origins. + void ResetAllOrigins(); + + static void RegisterUserPrefs(PrefService* user_prefs); + private: + void InitPrefs(); + void StartObserving(); + void StopObserving(); + + // Takes a notification object and shows it in the UI. + void ShowNotification(const Notification& notification); + + // Save a permission change to the profile. + void PersistPermissionChange(const GURL& origin, bool is_allowed); + + // Returns a display name for an origin, to be used in permission infobar + // or on the frame of the notification toast. Different from the origin + // itself when dealing with extensions. + std::wstring DisplayNameForOrigin(const GURL& origin); + + ContentSetting GetContentSetting(const GURL& origin); + + // The profile which owns this object. + Profile* profile_; + + // A cache of preferences which is accessible only on the IO thread + // to service synchronous IPCs. + scoped_refptr<NotificationsPrefsCache> prefs_cache_; + + // Non-owned pointer to the notification manager which manages the + // UI for desktop toasts. + NotificationUIManager* ui_manager_; + + DISALLOW_COPY_AND_ASSIGN(DesktopNotificationService); +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_DESKTOP_NOTIFICATION_SERVICE_H_ diff --git a/chrome/browser/notifications/desktop_notification_service_unittest.cc b/chrome/browser/notifications/desktop_notification_service_unittest.cc new file mode 100644 index 0000000..3af8b0f --- /dev/null +++ b/chrome/browser/notifications/desktop_notification_service_unittest.cc @@ -0,0 +1,246 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/desktop_notification_service.h" + +#include "base/ref_counted.h" +#include "base/waitable_event.h" +#include "chrome/browser/notifications/notifications_prefs_cache.h" +#include "chrome/browser/pref_service.h" +#include "chrome/browser/renderer_host/test/test_render_view_host.h" +#include "chrome/browser/scoped_pref_update.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/testing_profile.h" +#include "grit/generated_resources.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/WebKit/WebKit/chromium/public/WebNotificationPresenter.h" + +namespace { + +// NotificationsPrefsCache wants to be called on the IO thread. This class +// routes calls to the cache on the IO thread. +class ThreadProxy : public base::RefCountedThreadSafe<ThreadProxy> { + public: + ThreadProxy() + : io_event_(false, false), + ui_event_(false, false) { + // The current message loop was already initalized by the test superclass. + ui_thread_.reset( + new ChromeThread(ChromeThread::UI, MessageLoop::current())); + + // Create IO thread, start its message loop. + io_thread_.reset(new ChromeThread(ChromeThread::IO)); + io_thread_->Start(); + + // Calling PauseIOThread() here isn't safe, because the runnable method + // could complete before the constructor is done, deleting |this|. + } + + int CacheHasPermission(NotificationsPrefsCache* cache, const GURL& url) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + ChromeThread::PostTask(ChromeThread::IO, FROM_HERE, + NewRunnableMethod(this, &ThreadProxy::CacheHasPermissionIO, + cache, url)); + io_event_.Signal(); + ui_event_.Wait(); // Wait for IO thread to be done. + ChromeThread::PostTask(ChromeThread::IO, FROM_HERE, + NewRunnableMethod(this, &ThreadProxy::PauseIOThreadIO)); + + return permission_; + } + + void PauseIOThread() { + ChromeThread::PostTask(ChromeThread::IO, FROM_HERE, + NewRunnableMethod(this, &ThreadProxy::PauseIOThreadIO)); + } + + void DrainIOThread() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + io_event_.Signal(); + io_thread_->Stop(); + } + + private: + friend class base::RefCountedThreadSafe<ThreadProxy>; + ~ThreadProxy() { + DrainIOThread(); + } + + void PauseIOThreadIO() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO)); + io_event_.Wait(); + } + + void CacheHasPermissionIO(NotificationsPrefsCache* cache, const GURL& url) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO)); + permission_ = cache->HasPermission(url); + ui_event_.Signal(); + } + + base::WaitableEvent io_event_; + base::WaitableEvent ui_event_; + scoped_ptr<ChromeThread> ui_thread_; + scoped_ptr<ChromeThread> io_thread_; + + int permission_; +}; + + +class DesktopNotificationServiceTest : public RenderViewHostTestHarness { + public: + DesktopNotificationServiceTest() { + } + + virtual void SetUp() { + RenderViewHostTestHarness::SetUp(); + proxy_ = new ThreadProxy; + proxy_->PauseIOThread(); + + // Creates the service, calls InitPrefs() on it which loads data from the + // profile into the cache and then puts the cache in io thread mode. + service_ = profile()->GetDesktopNotificationService(); + cache_ = service_->prefs_cache(); + } + + virtual void TearDown() { + // The io thread's waiting on the io_event_ might hold a ref to |proxy_|, + // preventing its destruction. Clear that ref. + proxy_->DrainIOThread(); + RenderViewHostTestHarness::TearDown(); + } + + DesktopNotificationService* service_; + NotificationsPrefsCache* cache_; + scoped_refptr<ThreadProxy> proxy_; +}; + +TEST_F(DesktopNotificationServiceTest, DefaultContentSettingSentToCache) { + // The default pref registered in DesktopNotificationService is "ask", + // and that's what sent to the cache. + EXPECT_EQ(CONTENT_SETTING_ASK, cache_->CachedDefaultContentSetting()); + + // Change the default content setting. This will post a task on the IO thread + // to update the cache. + service_->SetDefaultContentSetting(CONTENT_SETTING_BLOCK); + + // The updated pref shouldn't be sent to the cache immediately. + EXPECT_EQ(CONTENT_SETTING_ASK, cache_->CachedDefaultContentSetting()); + + // Run IO thread tasks. + proxy_->DrainIOThread(); + + // Now that IO thread events have been processed, it should be there. + EXPECT_EQ(CONTENT_SETTING_BLOCK, cache_->CachedDefaultContentSetting()); +} + +TEST_F(DesktopNotificationServiceTest, GrantPermissionSentToCache) { + GURL url("http://allowed.com"); + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionNotAllowed, + proxy_->CacheHasPermission(cache_, url)); + + service_->GrantPermission(url); + + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionAllowed, + proxy_->CacheHasPermission(cache_, url)); +} + +TEST_F(DesktopNotificationServiceTest, DenyPermissionSentToCache) { + GURL url("http://denied.com"); + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionNotAllowed, + proxy_->CacheHasPermission(cache_, url)); + + service_->DenyPermission(url); + + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionDenied, + proxy_->CacheHasPermission(cache_, url)); +} + +TEST_F(DesktopNotificationServiceTest, PrefChangesSentToCache) { + PrefService* prefs = profile()->GetPrefs(); + + ListValue* allowed_sites = + prefs->GetMutableList(prefs::kDesktopNotificationAllowedOrigins); + { + allowed_sites->Append(new StringValue(GURL("http://allowed.com").spec())); + ScopedPrefUpdate updateAllowed( + prefs, prefs::kDesktopNotificationAllowedOrigins); + } + + ListValue* denied_sites = + prefs->GetMutableList(prefs::kDesktopNotificationDeniedOrigins); + { + denied_sites->Append(new StringValue(GURL("http://denied.com").spec())); + ScopedPrefUpdate updateDenied( + prefs, prefs::kDesktopNotificationDeniedOrigins); + } + + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionAllowed, + proxy_->CacheHasPermission(cache_, GURL("http://allowed.com"))); + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionDenied, + proxy_->CacheHasPermission(cache_, GURL("http://denied.com"))); +} + +TEST_F(DesktopNotificationServiceTest, GetAllowedOrigins) { + service_->GrantPermission(GURL("http://allowed2.com")); + service_->GrantPermission(GURL("http://allowed.com")); + + std::vector<GURL> allowed_origins(service_->GetAllowedOrigins()); + ASSERT_EQ(2u, allowed_origins.size()); + EXPECT_EQ(GURL("http://allowed2.com"), allowed_origins[0]); + EXPECT_EQ(GURL("http://allowed.com"), allowed_origins[1]); +} + +TEST_F(DesktopNotificationServiceTest, GetBlockedOrigins) { + service_->DenyPermission(GURL("http://denied2.com")); + service_->DenyPermission(GURL("http://denied.com")); + + std::vector<GURL> denied_origins(service_->GetBlockedOrigins()); + ASSERT_EQ(2u, denied_origins.size()); + EXPECT_EQ(GURL("http://denied2.com"), denied_origins[0]); + EXPECT_EQ(GURL("http://denied.com"), denied_origins[1]); +} + +TEST_F(DesktopNotificationServiceTest, ResetAllSentToCache) { + GURL allowed_url("http://allowed.com"); + service_->GrantPermission(allowed_url); + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionAllowed, + proxy_->CacheHasPermission(cache_, allowed_url)); + GURL denied_url("http://denied.com"); + service_->DenyPermission(denied_url); + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionDenied, + proxy_->CacheHasPermission(cache_, denied_url)); + + service_->ResetAllOrigins(); + + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionNotAllowed, + proxy_->CacheHasPermission(cache_, allowed_url)); + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionNotAllowed, + proxy_->CacheHasPermission(cache_, denied_url)); +} + +TEST_F(DesktopNotificationServiceTest, ResetAllowedSentToCache) { + GURL allowed_url("http://allowed.com"); + service_->GrantPermission(allowed_url); + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionAllowed, + proxy_->CacheHasPermission(cache_, allowed_url)); + + service_->ResetAllowedOrigin(allowed_url); + + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionNotAllowed, + proxy_->CacheHasPermission(cache_, allowed_url)); +} + +TEST_F(DesktopNotificationServiceTest, ResetBlockedSentToCache) { + GURL denied_url("http://denied.com"); + service_->DenyPermission(denied_url); + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionDenied, + proxy_->CacheHasPermission(cache_, denied_url)); + + service_->ResetBlockedOrigin(denied_url); + + EXPECT_EQ(WebKit::WebNotificationPresenter::PermissionNotAllowed, + proxy_->CacheHasPermission(cache_, denied_url)); +} + +} // namespace diff --git a/chrome/browser/notifications/desktop_notifications_unittest.cc b/chrome/browser/notifications/desktop_notifications_unittest.cc new file mode 100644 index 0000000..34746d1 --- /dev/null +++ b/chrome/browser/notifications/desktop_notifications_unittest.cc @@ -0,0 +1,327 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/desktop_notifications_unittest.h" + +#include "base/string_util.h" +#include "base/utf_string_conversions.h" + +// static +const int MockBalloonCollection::kMockBalloonSpace = 5; + +// static +std::string DesktopNotificationsTest::log_output_; + +void MockBalloonCollection::Add(const Notification& notification, + Profile* profile) { + // Swap in the logging proxy for the purpose of logging calls that + // would be made into javascript, then pass this down to the + // balloon collection. + Notification test_notification(notification.origin_url(), + notification.content_url(), + notification.display_source(), + string16(), /* replace_id */ + log_proxy_.get()); + BalloonCollectionImpl::Add(test_notification, profile); +} + +bool MockBalloonCollection::Remove(const Notification& notification) { + Notification test_notification(notification.origin_url(), + notification.content_url(), + notification.display_source(), + string16(), /* replace_id */ + log_proxy_.get()); + return BalloonCollectionImpl::Remove(test_notification); +} + +Balloon* MockBalloonCollection::MakeBalloon(const Notification& notification, + Profile* profile) { + // Start with a normal balloon but mock out the view. + Balloon* balloon = BalloonCollectionImpl::MakeBalloon(notification, profile); + balloon->set_view(new MockBalloonView(balloon)); + balloons_.push_back(balloon); + return balloon; +} + +void MockBalloonCollection::OnBalloonClosed(Balloon* source) { + std::deque<Balloon*>::iterator it; + for (it = balloons_.begin(); it != balloons_.end(); ++it) { + if (*it == source) { + balloons_.erase(it); + BalloonCollectionImpl::OnBalloonClosed(source); + break; + } + } +} + +int MockBalloonCollection::UppermostVerticalPosition() { + int min = 0; + std::deque<Balloon*>::iterator iter; + for (iter = balloons_.begin(); iter != balloons_.end(); ++iter) { + int pos = (*iter)->GetPosition().y(); + if (iter == balloons_.begin() || pos < min) + min = pos; + } + return min; +} + +DesktopNotificationsTest::DesktopNotificationsTest() + : ui_thread_(ChromeThread::UI, &message_loop_) { +} + +DesktopNotificationsTest::~DesktopNotificationsTest() { +} + +void DesktopNotificationsTest::SetUp() { + profile_.reset(new TestingProfile()); + balloon_collection_ = new MockBalloonCollection(); + ui_manager_.reset(new NotificationUIManager()); + ui_manager_->Initialize(balloon_collection_); + balloon_collection_->set_space_change_listener(ui_manager_.get()); + service_.reset(new DesktopNotificationService(profile(), ui_manager_.get())); + log_output_.clear(); +} + +void DesktopNotificationsTest::TearDown() { + service_.reset(NULL); + profile_.reset(NULL); + ui_manager_.reset(NULL); +} + +ViewHostMsg_ShowNotification_Params +DesktopNotificationsTest::StandardTestNotification() { + ViewHostMsg_ShowNotification_Params params; + params.notification_id = 0; + params.origin = GURL("http://www.google.com"); + params.is_html = false; + params.icon_url = GURL("/icon.png"); + params.title = ASCIIToUTF16("Title"); + params.body = ASCIIToUTF16("Text"); + params.direction = WebKit::WebTextDirectionDefault; + return params; +} + +TEST_F(DesktopNotificationsTest, TestShow) { + ViewHostMsg_ShowNotification_Params params = StandardTestNotification(); + params.notification_id = 1; + + EXPECT_TRUE(service_->ShowDesktopNotification( + params, 0, 0, DesktopNotificationService::PageNotification)); + MessageLoopForUI::current()->RunAllPending(); + EXPECT_EQ(1, balloon_collection_->count()); + + ViewHostMsg_ShowNotification_Params params2; + params2.origin = GURL("http://www.google.com"); + params2.is_html = true; + params2.contents_url = GURL("http://www.google.com/notification.html"); + params2.notification_id = 2; + + EXPECT_TRUE(service_->ShowDesktopNotification( + params2, 0, 0, DesktopNotificationService::PageNotification)); + MessageLoopForUI::current()->RunAllPending(); + EXPECT_EQ(2, balloon_collection_->count()); + + EXPECT_EQ("notification displayed\n" + "notification displayed\n", + log_output_); +} + +TEST_F(DesktopNotificationsTest, TestClose) { + ViewHostMsg_ShowNotification_Params params = StandardTestNotification(); + params.notification_id = 1; + + // Request a notification; should open a balloon. + EXPECT_TRUE(service_->ShowDesktopNotification( + params, 0, 0, DesktopNotificationService::PageNotification)); + MessageLoopForUI::current()->RunAllPending(); + EXPECT_EQ(1, balloon_collection_->count()); + + // Close all the open balloons. + while (balloon_collection_->count() > 0) { + (*(balloon_collection_->GetActiveBalloons().begin()))->OnClose(true); + } + + EXPECT_EQ("notification displayed\n" + "notification closed by user\n", + log_output_); +} + +TEST_F(DesktopNotificationsTest, TestCancel) { + int process_id = 0; + int route_id = 0; + int notification_id = 1; + + ViewHostMsg_ShowNotification_Params params = StandardTestNotification(); + params.notification_id = notification_id; + + // Request a notification; should open a balloon. + EXPECT_TRUE(service_->ShowDesktopNotification( + params, process_id, route_id, + DesktopNotificationService::PageNotification)); + MessageLoopForUI::current()->RunAllPending(); + EXPECT_EQ(1, balloon_collection_->count()); + + // Cancel the same notification + service_->CancelDesktopNotification(process_id, + route_id, + notification_id); + MessageLoopForUI::current()->RunAllPending(); + // Verify that the balloon collection is now empty. + EXPECT_EQ(0, balloon_collection_->count()); + + EXPECT_EQ("notification displayed\n" + "notification closed by script\n", + log_output_); +} + +#if defined(OS_WIN) || defined(TOOLKIT_VIEWS) +TEST_F(DesktopNotificationsTest, TestPositioning) { + ViewHostMsg_ShowNotification_Params params = StandardTestNotification(); + std::string expected_log; + // Create some toasts. After each but the first, make sure there + // is a minimum separation between the toasts. + int last_top = 0; + for (int id = 0; id <= 3; ++id) { + params.notification_id = id; + EXPECT_TRUE(service_->ShowDesktopNotification( + params, 0, 0, DesktopNotificationService::PageNotification)); + expected_log.append("notification displayed\n"); + int top = balloon_collection_->UppermostVerticalPosition(); + if (id > 0) + EXPECT_LE(top, last_top - balloon_collection_->MinHeight()); + last_top = top; + } + + EXPECT_EQ(expected_log, log_output_); +} + +TEST_F(DesktopNotificationsTest, TestVariableSize) { + ViewHostMsg_ShowNotification_Params params; + params.origin = GURL("http://long.google.com"); + params.is_html = false; + params.icon_url = GURL("/icon.png"); + params.title = ASCIIToUTF16("Really Really Really Really Really Really " + "Really Really Really Really Really Really " + "Really Really Really Really Really Really " + "Really Long Title"), + params.body = ASCIIToUTF16("Text"); + params.notification_id = 0; + + std::string expected_log; + // Create some toasts. After each but the first, make sure there + // is a minimum separation between the toasts. + EXPECT_TRUE(service_->ShowDesktopNotification( + params, 0, 0, DesktopNotificationService::PageNotification)); + expected_log.append("notification displayed\n"); + + params.origin = GURL("http://short.google.com"); + params.title = ASCIIToUTF16("Short title"); + params.notification_id = 1; + EXPECT_TRUE(service_->ShowDesktopNotification( + params, 0, 0, DesktopNotificationService::PageNotification)); + expected_log.append("notification displayed\n"); + + std::deque<Balloon*>& balloons = balloon_collection_->balloons(); + std::deque<Balloon*>::iterator iter; + for (iter = balloons.begin(); iter != balloons.end(); ++iter) { + if ((*iter)->notification().origin_url().host() == "long.google.com") { + EXPECT_GE((*iter)->GetViewSize().height(), + balloon_collection_->MinHeight()); + EXPECT_LE((*iter)->GetViewSize().height(), + balloon_collection_->MaxHeight()); + } else { + EXPECT_EQ((*iter)->GetViewSize().height(), + balloon_collection_->MinHeight()); + } + } + EXPECT_EQ(expected_log, log_output_); +} +#endif + +TEST_F(DesktopNotificationsTest, TestQueueing) { + int process_id = 0; + int route_id = 0; + + // Request lots of identical notifications. + ViewHostMsg_ShowNotification_Params params = StandardTestNotification(); + const int kLotsOfToasts = 20; + for (int id = 1; id <= kLotsOfToasts; ++id) { + params.notification_id = id; + EXPECT_TRUE(service_->ShowDesktopNotification( + params, process_id, route_id, + DesktopNotificationService::PageNotification)); + } + MessageLoopForUI::current()->RunAllPending(); + + // Build up an expected log of what should be happening. + std::string expected_log; + for (int i = 0; i < balloon_collection_->max_balloon_count(); ++i) { + expected_log.append("notification displayed\n"); + } + + // The max number that our balloon collection can hold should be + // shown. + EXPECT_EQ(balloon_collection_->max_balloon_count(), + balloon_collection_->count()); + EXPECT_EQ(expected_log, log_output_); + + // Cancel the notifications from the start; the balloon space should + // remain full. + int id; + for (id = 1; + id <= kLotsOfToasts - balloon_collection_->max_balloon_count(); + ++id) { + service_->CancelDesktopNotification(process_id, route_id, id); + MessageLoopForUI::current()->RunAllPending(); + expected_log.append("notification closed by script\n"); + expected_log.append("notification displayed\n"); + EXPECT_EQ(balloon_collection_->max_balloon_count(), + balloon_collection_->count()); + EXPECT_EQ(expected_log, log_output_); + } + + // Now cancel the rest. It should empty the balloon space. + for (; id <= kLotsOfToasts; ++id) { + service_->CancelDesktopNotification(process_id, route_id, id); + expected_log.append("notification closed by script\n"); + MessageLoopForUI::current()->RunAllPending(); + EXPECT_EQ(expected_log, log_output_); + } + + // Verify that the balloon collection is now empty. + EXPECT_EQ(0, balloon_collection_->count()); +} + +TEST_F(DesktopNotificationsTest, TestEarlyDestruction) { + // Create some toasts and then prematurely delete the notification service, + // just to make sure nothing crashes/leaks. + ViewHostMsg_ShowNotification_Params params = StandardTestNotification(); + for (int id = 0; id <= 3; ++id) { + params.notification_id = id; + EXPECT_TRUE(service_->ShowDesktopNotification( + params, 0, 0, DesktopNotificationService::PageNotification)); + } + service_.reset(NULL); +} + +TEST_F(DesktopNotificationsTest, TestUserInputEscaping) { + // Create a test script with some HTML; assert that it doesn't get into the + // data:// URL that's produced for the balloon. + ViewHostMsg_ShowNotification_Params params = StandardTestNotification(); + params.title = ASCIIToUTF16("<script>window.alert('uh oh');</script>"); + params.body = ASCIIToUTF16("<i>this text is in italics</i>"); + params.notification_id = 1; + EXPECT_TRUE(service_->ShowDesktopNotification( + params, 0, 0, DesktopNotificationService::PageNotification)); + + MessageLoopForUI::current()->RunAllPending(); + EXPECT_EQ(1, balloon_collection_->count()); + Balloon* balloon = (*balloon_collection_->balloons().begin()); + GURL data_url = balloon->notification().content_url(); + EXPECT_EQ(std::string::npos, data_url.spec().find("<script>")); + EXPECT_EQ(std::string::npos, data_url.spec().find("<i>")); + // URL-encoded versions of tags should also not be found. + EXPECT_EQ(std::string::npos, data_url.spec().find("%3cscript%3e")); + EXPECT_EQ(std::string::npos, data_url.spec().find("%3ci%3e")); +} diff --git a/chrome/browser/notifications/desktop_notifications_unittest.h b/chrome/browser/notifications/desktop_notifications_unittest.h new file mode 100644 index 0000000..56a2a6b --- /dev/null +++ b/chrome/browser/notifications/desktop_notifications_unittest.h @@ -0,0 +1,119 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_NOTIFICATIONS_DESKTOP_NOTIFICATIONS_UNITTEST_H_ +#define CHROME_BROWSER_NOTIFICATIONS_DESKTOP_NOTIFICATIONS_UNITTEST_H_ + +#include <deque> +#include <string> + +#include "base/message_loop.h" +#include "chrome/browser/notifications/balloon_collection_impl.h" +#include "chrome/browser/notifications/desktop_notification_service.h" +#include "chrome/browser/notifications/notification.h" +#include "chrome/browser/notifications/notification_test_util.h" +#include "chrome/browser/notifications/notification_ui_manager.h" +#include "chrome/browser/notifications/notifications_prefs_cache.h" +#include "chrome/common/render_messages.h" +#include "chrome/test/testing_profile.h" +#include "testing/gtest/include/gtest/gtest.h" + +class DesktopNotificationsTest; +typedef LoggingNotificationProxyBase<DesktopNotificationsTest> + LoggingNotificationProxy; + +// Test version of the balloon collection which counts the number +// of notifications that are added to it. +class MockBalloonCollection : public BalloonCollectionImpl { + public: + MockBalloonCollection() : + log_proxy_(new LoggingNotificationProxy()) {} + + // Our mock collection has an area large enough for a fixed number + // of balloons. + static const int kMockBalloonSpace; + int max_balloon_count() const { return kMockBalloonSpace; } + + // BalloonCollectionImpl overrides + virtual void Add(const Notification& notification, + Profile* profile); + virtual bool Remove(const Notification& notification); + virtual bool HasSpace() const { return count() < kMockBalloonSpace; } + virtual Balloon* MakeBalloon(const Notification& notification, + Profile* profile); + virtual void DisplayChanged() {} + virtual void OnBalloonClosed(Balloon* source); + virtual const BalloonCollection::Balloons& GetActiveBalloons() { + return balloons_; + } + + // Number of balloons being shown. + std::deque<Balloon*>& balloons() { return balloons_; } + int count() const { return balloons_.size(); } + + // Returns the highest y-coordinate of all the balloons in the collection. + int UppermostVerticalPosition(); + + // Returns the height bounds of a balloon. + int MinHeight() { return Layout::min_balloon_height(); } + int MaxHeight() { return Layout::max_balloon_height(); } + + private: + std::deque<Balloon*> balloons_; + scoped_refptr<LoggingNotificationProxy> log_proxy_; +}; + +class DesktopNotificationsTest : public testing::Test { + public: + DesktopNotificationsTest(); + ~DesktopNotificationsTest(); + + static void log(const std::string& message) { + log_output_.append(message); + } + + Profile* profile() { return profile_.get(); } + + protected: + // testing::Test overrides + virtual void SetUp(); + virtual void TearDown(); + + void AllowOrigin(const GURL& origin) { + service_->GrantPermission(origin); + } + + void DenyOrigin(const GURL& origin) { + service_->DenyPermission(origin); + } + + int HasPermission(const GURL& origin) { + return service_->prefs_cache()->HasPermission(origin); + } + + // Constructs a notification parameter structure for use in tests. + ViewHostMsg_ShowNotification_Params StandardTestNotification(); + + // Create a message loop to allow notifications code to post tasks, + // and a thread so that notifications code runs on the expected thread. + MessageLoopForUI message_loop_; + ChromeThread ui_thread_; + + // Test profile. + scoped_ptr<TestingProfile> profile_; + + // Mock balloon collection -- owned by the NotificationUIManager + MockBalloonCollection* balloon_collection_; + + // Real UI manager. + scoped_ptr<NotificationUIManager> ui_manager_; + + // Real DesktopNotificationService + scoped_ptr<DesktopNotificationService> service_; + + // Contains the cumulative output of the unit test. + static std::string log_output_; +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_DESKTOP_NOTIFICATIONS_UNITTEST_H_ diff --git a/chrome/browser/notifications/notification.h b/chrome/browser/notifications/notification.h new file mode 100644 index 0000000..bdad291 --- /dev/null +++ b/chrome/browser/notifications/notification.h @@ -0,0 +1,88 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_H_ +#define CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_H_ + +#include "base/basictypes.h" +#include "chrome/browser/notifications/notification_object_proxy.h" +#include "googleurl/src/gurl.h" + +class NotificationDelegate; + +// Representation of an notification to be shown to the user. All +// notifications at this level are HTML, although they may be +// data: URLs representing simple text+icon notifications. +class Notification { + public: + // TODO: http://crbug.com/43899 Convert this class to string16. + Notification(const GURL& origin_url, const GURL& content_url, + const std::wstring& display_source, + const string16& replace_id, + NotificationDelegate* delegate) + : origin_url_(origin_url), + content_url_(content_url), + display_source_(display_source), + replace_id_(replace_id), + delegate_(delegate) { + } + + Notification(const Notification& notification) + : origin_url_(notification.origin_url()), + content_url_(notification.content_url()), + display_source_(notification.display_source()), + replace_id_(notification.replace_id()), + delegate_(notification.delegate()) { + } + + void operator=(const Notification& notification) { + origin_url_ = notification.origin_url(); + content_url_ = notification.content_url(); + display_source_ = notification.display_source(); + replace_id_ = notification.replace_id(); + delegate_ = notification.delegate(); + } + + // The URL (may be data:) containing the contents for the notification. + const GURL& content_url() const { return content_url_; } + + // The origin URL of the script which requested the notification. + const GURL& origin_url() const { return origin_url_; } + + // A display string for the source of the notification. + const std::wstring& display_source() const { return display_source_; } + + const string16& replace_id() const { return replace_id_; } + + void Display() const { delegate()->Display(); } + void Error() const { delegate()->Error(); } + void Close(bool by_user) const { delegate()->Close(by_user); } + + bool IsSame(const Notification& other) const { + return delegate()->id() == other.delegate()->id(); + } + + private: + NotificationDelegate* delegate() const { return delegate_.get(); } + + // The Origin of the page/worker which created this notification. + GURL origin_url_; + + // The URL of the HTML content of the toast (may be a data: URL for simple + // string-based notifications). + GURL content_url_; + + // The display string for the source of the notification. Could be + // the same as origin_url_, or the name of an extension. + std::wstring display_source_; + + // The replace ID for the notification. + string16 replace_id_; + + // A proxy object that allows access back to the JavaScript object that + // represents the notification, for firing events. + scoped_refptr<NotificationDelegate> delegate_; +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_H_ diff --git a/chrome/browser/notifications/notification_delegate.h b/chrome/browser/notifications/notification_delegate.h new file mode 100644 index 0000000..6d69335 --- /dev/null +++ b/chrome/browser/notifications/notification_delegate.h @@ -0,0 +1,34 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_DELEGATE_H_ +#define CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_DELEGATE_H_ + +#include <string> + +#include "base/ref_counted.h" + +// Delegate for a notification. This class has two role, to implement +// callback methods for notification, and provides an identify of +// the associated notification. +class NotificationDelegate + : public base::RefCountedThreadSafe<NotificationDelegate> { + public: + + // To be called when the desktop notification is actually shown. + virtual void Display() = 0; + + // To be called when the desktop notification cannot be shown due to an + // error. + virtual void Error() = 0; + + // To be called when the desktop notification is closed. If closed by a + // user explicitly (as opposed to timeout/script), |by_user| should be true. + virtual void Close(bool by_user) = 0; + + // Returns unique id of the notification. + virtual std::string id() const = 0; +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_DELEGATE_H_ diff --git a/chrome/browser/notifications/notification_exceptions_table_model.cc b/chrome/browser/notifications/notification_exceptions_table_model.cc new file mode 100644 index 0000000..fdb08f4 --- /dev/null +++ b/chrome/browser/notifications/notification_exceptions_table_model.cc @@ -0,0 +1,101 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/notification_exceptions_table_model.h" + +#include "app/l10n_util.h" +#include "app/l10n_util_collator.h" +#include "app/table_model_observer.h" +#include "base/utf_string_conversions.h" +#include "chrome/common/content_settings_helper.h" +#include "chrome/common/url_constants.h" +#include "grit/generated_resources.h" + +NotificationExceptionsTableModel::NotificationExceptionsTableModel( + DesktopNotificationService* service) + : service_(service), + observer_(NULL) { + std::vector<GURL> allowed(service_->GetAllowedOrigins()); + std::vector<GURL> blocked(service_->GetBlockedOrigins()); + entries_.reserve(allowed.size() + blocked.size()); + for (size_t i = 0; i < allowed.size(); ++i) + entries_.push_back(Entry(allowed[i], CONTENT_SETTING_ALLOW)); + for (size_t i = 0; i < blocked.size(); ++i) + entries_.push_back(Entry(blocked[i], CONTENT_SETTING_BLOCK)); + sort(entries_.begin(), entries_.end()); +} + +bool NotificationExceptionsTableModel::CanRemoveRows( + const Rows& rows) const { + return !rows.empty(); +} + +void NotificationExceptionsTableModel::RemoveRows(const Rows& rows) { + // This is O(n^2) in rows.size(). Since n is small, that's ok. + for (Rows::const_reverse_iterator i(rows.rbegin()); i != rows.rend(); ++i) { + size_t row = *i; + Entry* entry = &entries_[row]; + if (entry->setting == CONTENT_SETTING_ALLOW) { + service_->ResetAllowedOrigin(entry->origin); + } else { + DCHECK_EQ(entry->setting, CONTENT_SETTING_BLOCK); + service_->ResetBlockedOrigin(entry->origin); + } + entries_.erase(entries_.begin() + row); // Note: |entry| is now garbage. + if (observer_) + observer_->OnItemsRemoved(row, 1); + } +} + +void NotificationExceptionsTableModel::RemoveAll() { + int old_row_count = RowCount(); + entries_.clear(); + service_->ResetAllOrigins(); + if (observer_) + observer_->OnItemsRemoved(0, old_row_count); +} + +int NotificationExceptionsTableModel::RowCount() { + return static_cast<int>(entries_.size()); +} + +std::wstring NotificationExceptionsTableModel::GetText(int row, + int column_id) { + const Entry& entry = entries_[row]; + if (column_id == IDS_EXCEPTIONS_HOSTNAME_HEADER) { + return content_settings_helper::OriginToWString(entry.origin); + } + + if (column_id == IDS_EXCEPTIONS_ACTION_HEADER) { + switch (entry.setting) { + case CONTENT_SETTING_ALLOW: + return l10n_util::GetString(IDS_EXCEPTIONS_ALLOW_BUTTON); + case CONTENT_SETTING_BLOCK: + return l10n_util::GetString(IDS_EXCEPTIONS_BLOCK_BUTTON); + default: + break; + } + } + + NOTREACHED(); + return std::wstring(); +} + +void NotificationExceptionsTableModel::SetObserver( + TableModelObserver* observer) { + observer_ = observer; +} + +NotificationExceptionsTableModel::Entry::Entry( + const GURL& in_origin, + ContentSetting in_setting) + : origin(in_origin), + setting(in_setting) { +} + +bool NotificationExceptionsTableModel::Entry::operator<( + const NotificationExceptionsTableModel::Entry& b) const { + DCHECK_NE(origin, b.origin); + return origin < b.origin; +} diff --git a/chrome/browser/notifications/notification_exceptions_table_model.h b/chrome/browser/notifications/notification_exceptions_table_model.h new file mode 100644 index 0000000..01137f7 --- /dev/null +++ b/chrome/browser/notifications/notification_exceptions_table_model.h @@ -0,0 +1,51 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_EXCEPTIONS_TABLE_MODEL_H_ +#define CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_EXCEPTIONS_TABLE_MODEL_H_ + +#include <set> +#include <vector> + +#include "chrome/browser/notifications/desktop_notification_service.h" +#include "chrome/browser/remove_rows_table_model.h" +#include "chrome/common/content_settings.h" +#include "chrome/common/content_settings_types.h" + +class NotificationExceptionsTableModel : public RemoveRowsTableModel { + public: + explicit NotificationExceptionsTableModel( + DesktopNotificationService* service); + + // RemoveRowsTableModel overrides: + virtual bool CanRemoveRows(const Rows& rows) const; + virtual void RemoveRows(const Rows& rows); + virtual void RemoveAll(); + + // TableModel overrides: + virtual int RowCount(); + virtual std::wstring GetText(int row, int column_id); + virtual void SetObserver(TableModelObserver* observer); + + private: + struct Entry { + Entry(const GURL& origin, ContentSetting setting); + bool operator<(const Entry& b) const; + + GURL origin; + ContentSetting setting; + }; + + DesktopNotificationService* service_; + + typedef std::vector<Entry> EntriesVector; + EntriesVector entries_; + + TableModelObserver* observer_; + + DISALLOW_COPY_AND_ASSIGN(NotificationExceptionsTableModel); +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_EXCEPTIONS_TABLE_MODEL_H_ + diff --git a/chrome/browser/notifications/notification_exceptions_table_model_unittest.cc b/chrome/browser/notifications/notification_exceptions_table_model_unittest.cc new file mode 100644 index 0000000..6919b0b --- /dev/null +++ b/chrome/browser/notifications/notification_exceptions_table_model_unittest.cc @@ -0,0 +1,125 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/notification_exceptions_table_model.h" + +#include "app/l10n_util.h" +#include "chrome/browser/renderer_host/test/test_render_view_host.h" +#include "chrome/test/testing_profile.h" +#include "grit/generated_resources.h" +#include "testing/gtest/include/gtest/gtest.h" + +class NotificationExceptionsTableModelTest : public RenderViewHostTestHarness { + public: + NotificationExceptionsTableModelTest() + : ui_thread_(ChromeThread::UI, MessageLoop::current()) { + } + + virtual ~NotificationExceptionsTableModelTest() { + } + + virtual void SetUp() { + RenderViewHostTestHarness::SetUp(); + service_ = profile()->GetDesktopNotificationService(); + ResetModel(); + } + + virtual void TearDown() { + model_.reset(NULL); + RenderViewHostTestHarness::TearDown(); + } + + virtual void ResetModel() { + model_.reset(new NotificationExceptionsTableModel(service_)); + } + + virtual void FillData() { + service_->GrantPermission(GURL("http://e-allowed2.com")); + service_->GrantPermission(GURL("http://allowed.com")); + + service_->DenyPermission(GURL("http://denied2.com")); + service_->DenyPermission(GURL("http://denied.com")); + service_->DenyPermission(GURL("http://f-denied3.com")); + + ResetModel(); + } + + protected: + ChromeThread ui_thread_; + scoped_ptr<NotificationExceptionsTableModel> model_; + DesktopNotificationService* service_; +}; + +TEST_F(NotificationExceptionsTableModelTest, CanCreate) { + EXPECT_EQ(0, model_->RowCount()); +} + +TEST_F(NotificationExceptionsTableModelTest, RemoveAll) { + FillData(); + EXPECT_EQ(2u, service_->GetAllowedOrigins().size()); + EXPECT_EQ(3u, service_->GetBlockedOrigins().size()); + EXPECT_EQ(5, model_->RowCount()); + + model_->RemoveAll(); + EXPECT_EQ(0, model_->RowCount()); + + EXPECT_EQ(0u, service_->GetAllowedOrigins().size()); + EXPECT_EQ(0u, service_->GetBlockedOrigins().size()); +} + +TEST_F(NotificationExceptionsTableModelTest, AlphabeticalOrder) { + FillData(); + EXPECT_EQ(5, model_->RowCount()); + + EXPECT_EQ(L"allowed.com", + model_->GetText(0, IDS_EXCEPTIONS_HOSTNAME_HEADER)); + EXPECT_EQ(l10n_util::GetString(IDS_EXCEPTIONS_ALLOW_BUTTON), + model_->GetText(0, IDS_EXCEPTIONS_ACTION_HEADER)); + + EXPECT_EQ(L"denied.com", + model_->GetText(1, IDS_EXCEPTIONS_HOSTNAME_HEADER)); + EXPECT_EQ(l10n_util::GetString(IDS_EXCEPTIONS_BLOCK_BUTTON), + model_->GetText(1, IDS_EXCEPTIONS_ACTION_HEADER)); + + EXPECT_EQ(L"denied2.com", + model_->GetText(2, IDS_EXCEPTIONS_HOSTNAME_HEADER)); + EXPECT_EQ(l10n_util::GetString(IDS_EXCEPTIONS_BLOCK_BUTTON), + model_->GetText(2, IDS_EXCEPTIONS_ACTION_HEADER)); + + EXPECT_EQ(L"e-allowed2.com", + model_->GetText(3, IDS_EXCEPTIONS_HOSTNAME_HEADER)); + EXPECT_EQ(l10n_util::GetString(IDS_EXCEPTIONS_ALLOW_BUTTON), + model_->GetText(3, IDS_EXCEPTIONS_ACTION_HEADER)); + + EXPECT_EQ(L"f-denied3.com", + model_->GetText(4, IDS_EXCEPTIONS_HOSTNAME_HEADER)); + EXPECT_EQ(l10n_util::GetString(IDS_EXCEPTIONS_BLOCK_BUTTON), + model_->GetText(4, IDS_EXCEPTIONS_ACTION_HEADER)); +} + +TEST_F(NotificationExceptionsTableModelTest, RemoveRows) { + FillData(); + EXPECT_EQ(5, model_->RowCount()); + + { + RemoveRowsTableModel::Rows rows; + rows.insert(0); // allowed.com + rows.insert(3); // e-allowed2.com + model_->RemoveRows(rows); + } + EXPECT_EQ(3, model_->RowCount()); + EXPECT_EQ(0u, service_->GetAllowedOrigins().size()); + EXPECT_EQ(3u, service_->GetBlockedOrigins().size()); + + { + RemoveRowsTableModel::Rows rows; + rows.insert(0); + rows.insert(1); + rows.insert(2); + model_->RemoveRows(rows); + } + EXPECT_EQ(0, model_->RowCount()); + EXPECT_EQ(0u, service_->GetAllowedOrigins().size()); + EXPECT_EQ(0u, service_->GetBlockedOrigins().size()); +} diff --git a/chrome/browser/notifications/notification_object_proxy.cc b/chrome/browser/notifications/notification_object_proxy.cc new file mode 100644 index 0000000..76363e5 --- /dev/null +++ b/chrome/browser/notifications/notification_object_proxy.cc @@ -0,0 +1,77 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/notification_object_proxy.h" + +#include "base/message_loop.h" +#include "base/string16.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/renderer_host/render_view_host.h" +#include "chrome/common/render_messages.h" + +NotificationObjectProxy::NotificationObjectProxy(int process_id, int route_id, + int notification_id, bool worker) + : process_id_(process_id), + route_id_(route_id), + notification_id_(notification_id), + worker_(worker) { +} + +void NotificationObjectProxy::Display() { + if (worker_) { + // TODO(johnnyg): http://crbug.com/23065 Worker support coming soon. + NOTREACHED(); + } else { + DeliverMessage(new ViewMsg_PostDisplayToNotificationObject( + route_id_, notification_id_)); + } +} + +void NotificationObjectProxy::Error() { + if (worker_) { + // TODO(johnnyg): http://crbug.com/23065 Worker support coming soon. + NOTREACHED(); + } else { + DeliverMessage(new ViewMsg_PostErrorToNotificationObject( + route_id_, notification_id_, string16())); + } +} + +void NotificationObjectProxy::Close(bool by_user) { + if (worker_) { + // TODO(johnnyg): http://crbug.com/23065 Worker support coming soon. + NOTREACHED(); + } else { + DeliverMessage(new ViewMsg_PostCloseToNotificationObject( + route_id_, notification_id_, by_user)); + } +} + +std::string NotificationObjectProxy::id() const { + return StringPrintf("%d:%d:%d:%d", process_id_, route_id_, + notification_id_, worker_); +} + + +void NotificationObjectProxy::DeliverMessage(IPC::Message* message) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + ChromeThread::PostTask( + ChromeThread::IO, FROM_HERE, + NewRunnableMethod(this, &NotificationObjectProxy::Send, message)); +} + +// Deferred method which runs on the IO thread and sends a message to the +// proxied notification, routing it through the correct host in the browser. +void NotificationObjectProxy::Send(IPC::Message* message) { + // Take ownership of the message; ownership will pass to a host if possible. + scoped_ptr<IPC::Message> owned_message(message); + + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO)); + RenderViewHost* host = RenderViewHost::FromID(process_id_, route_id_); + if (host) { + // Pass ownership to the host. + host->Send(owned_message.release()); + } +} diff --git a/chrome/browser/notifications/notification_object_proxy.h b/chrome/browser/notifications/notification_object_proxy.h new file mode 100644 index 0000000..a956b3a --- /dev/null +++ b/chrome/browser/notifications/notification_object_proxy.h @@ -0,0 +1,53 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_OBJECT_PROXY_H_ +#define CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_OBJECT_PROXY_H_ + +#include <string> + +#include "chrome/browser/notifications/notification_delegate.h" + +class MessageLoop; +namespace IPC { +class Message; +} + +// A NotificationObjectProxy stands in for the JavaScript Notification object +// which corresponds to a notification toast on the desktop. It can be signaled +// when various events occur regarding the desktop notification, and the +// attached JS listeners will be invoked in the renderer or worker process. +class NotificationObjectProxy + : public NotificationDelegate { + public: + // Creates a Proxy object with the necessary callback information. + NotificationObjectProxy(int process_id, int route_id, + int notification_id, bool worker); + + // NotificationDelegate implementation. + virtual void Display(); + virtual void Error(); + virtual void Close(bool by_user); + virtual std::string id() const; + + protected: + friend class base::RefCountedThreadSafe<NotificationObjectProxy>; + + virtual ~NotificationObjectProxy() {} + + private: + // Called on UI thread to schedule a message for sending. + void DeliverMessage(IPC::Message* message); + + // Called via Task on IO thread to actually send a message to a notification. + void Send(IPC::Message* message); + + // Callback information to find the JS Notification object where it lives. + int process_id_; + int route_id_; + int notification_id_; + bool worker_; +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_OBJECT_PROXY_H_ diff --git a/chrome/browser/notifications/notification_options_menu_model.cc b/chrome/browser/notifications/notification_options_menu_model.cc new file mode 100644 index 0000000..66379fa --- /dev/null +++ b/chrome/browser/notifications/notification_options_menu_model.cc @@ -0,0 +1,96 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/notification_options_menu_model.h" + +#include "app/l10n_util.h" +#include "base/compiler_specific.h" +#include "base/logging.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/browser_list.h" +#include "chrome/browser/extensions/extensions_service.h" +#include "chrome/browser/notifications/desktop_notification_service.h" +#include "chrome/browser/profile.h" +#include "chrome/common/content_settings_types.h" +#include "chrome/common/extensions/extension.h" +#include "chrome/common/url_constants.h" +#include "grit/generated_resources.h" + +// Menu commands +const int kRevokePermissionCommand = 0; +const int kDisableExtensionCommand = 1; +const int kOpenContentSettingsCommand = 2; + +NotificationOptionsMenuModel::NotificationOptionsMenuModel(Balloon* balloon) + : ALLOW_THIS_IN_INITIALIZER_LIST(menus::SimpleMenuModel(this)), + balloon_(balloon) { + + const Notification& notification = balloon->notification(); + const GURL& origin = notification.origin_url(); + + if (origin.SchemeIs(chrome::kExtensionScheme)) { + const string16 disable_label = l10n_util::GetStringUTF16( + IDS_EXTENSIONS_DISABLE); + AddItem(kDisableExtensionCommand, disable_label); + } else { + const string16 disable_label = l10n_util::GetStringFUTF16( + IDS_NOTIFICATION_BALLOON_REVOKE_MESSAGE, + WideToUTF16(notification.display_source())); + AddItem(kRevokePermissionCommand, disable_label); + } + + const string16 settings_label = l10n_util::GetStringUTF16( + IDS_NOTIFICATIONS_SETTINGS_BUTTON); + AddItem(kOpenContentSettingsCommand, settings_label); +} + +NotificationOptionsMenuModel::~NotificationOptionsMenuModel() { +} + +bool NotificationOptionsMenuModel::IsCommandIdChecked(int /* command_id */) + const { + // Nothing in the menu is checked. + return false; +} + +bool NotificationOptionsMenuModel::IsCommandIdEnabled(int /* command_id */) + const { + // All the menu options are always enabled. + return true; +} + +bool NotificationOptionsMenuModel::GetAcceleratorForCommandId( + int /* command_id */, menus::Accelerator* /* accelerator */) { + // Currently no accelerators. + return false; +} + +void NotificationOptionsMenuModel::ExecuteCommand(int command_id) { + DesktopNotificationService* service = + balloon_->profile()->GetDesktopNotificationService(); + ExtensionsService* ext_service = + balloon_->profile()->GetExtensionsService(); + const GURL& origin = balloon_->notification().origin_url(); + switch (command_id) { + case kRevokePermissionCommand: + service->DenyPermission(origin); + break; + case kDisableExtensionCommand: { + Extension* extension = ext_service->GetExtensionByURL(origin); + if (extension) + ext_service->DisableExtension(extension->id()); + break; + } + case kOpenContentSettingsCommand: { + Browser* browser = BrowserList::GetLastActive(); + if (browser) + static_cast<TabContentsDelegate*>(browser)->ShowContentSettingsWindow( + CONTENT_SETTINGS_TYPE_NOTIFICATIONS); + break; + } + default: + NOTREACHED(); + break; + } +} diff --git a/chrome/browser/notifications/notification_options_menu_model.h b/chrome/browser/notifications/notification_options_menu_model.h new file mode 100644 index 0000000..751c799 --- /dev/null +++ b/chrome/browser/notifications/notification_options_menu_model.h @@ -0,0 +1,30 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_OPTIONS_MENU_MODEL_H_ +#define CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_OPTIONS_MENU_MODEL_H_ + +#include "app/menus/simple_menu_model.h" +#include "chrome/browser/notifications/balloon.h" + +class NotificationOptionsMenuModel : public menus::SimpleMenuModel, + public menus::SimpleMenuModel::Delegate { + public: + explicit NotificationOptionsMenuModel(Balloon* balloon); + virtual ~NotificationOptionsMenuModel(); + + // Overridden from menus::SimpleMenuModel::Delegate: + virtual bool IsCommandIdChecked(int command_id) const; + virtual bool IsCommandIdEnabled(int command_id) const; + virtual bool GetAcceleratorForCommandId(int command_id, + menus::Accelerator* accelerator); + virtual void ExecuteCommand(int command_id); + + private: + Balloon* balloon_; // Not owned. + + DISALLOW_COPY_AND_ASSIGN(NotificationOptionsMenuModel); +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_OPTIONS_MENU_MODEL_H_ diff --git a/chrome/browser/notifications/notification_test_util.h b/chrome/browser/notifications/notification_test_util.h new file mode 100644 index 0000000..d3ad7ee --- /dev/null +++ b/chrome/browser/notifications/notification_test_util.h @@ -0,0 +1,54 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_TEST_UTIL_H_ +#define CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_TEST_UTIL_H_ + +#include "chrome/browser/notifications/notification_object_proxy.h" +#include "chrome/browser/notifications/balloon.h" +#include "gfx/size.h" + +// Mock implementation of Javascript object proxy which logs events that +// would have been fired on it. |Logger| class must static "log()" method. +template<class Logger> +class LoggingNotificationProxyBase : public NotificationObjectProxy { + public: + LoggingNotificationProxyBase() : + NotificationObjectProxy(0, 0, 0, false) {} + + // NotificationObjectProxy override + virtual void Display() { + Logger::log("notification displayed\n"); + } + virtual void Error() { + Logger::log("notification error\n"); + } + virtual void Close(bool by_user) { + if (by_user) + Logger::log("notification closed by user\n"); + else + Logger::log("notification closed by script\n"); + } +}; + +// Test version of a balloon view which doesn't do anything +// viewable, but does know how to close itself the same as a regular +// BalloonView. +class MockBalloonView : public BalloonView { + public: + explicit MockBalloonView(Balloon * balloon) : + balloon_(balloon) {} + void Show(Balloon* balloon) {} + void Update() {} + void RepositionToBalloon() {} + void Close(bool by_user) { balloon_->OnClose(by_user); } + gfx::Size GetSize() const { return balloon_->content_size(); } + BalloonHost* GetHost() const { return NULL; } + + private: + // Non-owned pointer. + Balloon* balloon_; +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_TEST_UTIL_H_ diff --git a/chrome/browser/notifications/notification_ui_manager.cc b/chrome/browser/notifications/notification_ui_manager.cc new file mode 100644 index 0000000..42b9c5e --- /dev/null +++ b/chrome/browser/notifications/notification_ui_manager.cc @@ -0,0 +1,132 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/notification_ui_manager.h" + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/stl_util-inl.h" +#include "chrome/browser/notifications/balloon_collection.h" +#include "chrome/browser/notifications/notification.h" +#include "chrome/browser/renderer_host/site_instance.h" + +// A class which represents a notification waiting to be shown. +class QueuedNotification { + public: + QueuedNotification(const Notification& notification, Profile* profile) + : notification_(notification), + profile_(profile) { + } + + const Notification& notification() const { return notification_; } + Profile* profile() const { return profile_; } + + void Replace(const Notification& new_notification) { + notification_ = new_notification; + } + + private: + // The notification to be shown. + Notification notification_; + + // Non owned pointer to the user's profile. + Profile* profile_; + + DISALLOW_COPY_AND_ASSIGN(QueuedNotification); +}; + +NotificationUIManager::NotificationUIManager() + : balloon_collection_(NULL) { +} + +NotificationUIManager::~NotificationUIManager() { + STLDeleteElements(&show_queue_); +} + +// static +NotificationUIManager* NotificationUIManager::Create() { + BalloonCollection* balloons = BalloonCollection::Create(); + NotificationUIManager* instance = new NotificationUIManager(); + instance->Initialize(balloons); + balloons->set_space_change_listener(instance); + return instance; +} + +void NotificationUIManager::Add(const Notification& notification, + Profile* profile) { + if (TryReplacement(notification)) { + return; + } + + LOG(INFO) << "Added notification. URL: " + << notification.content_url().spec().c_str(); + show_queue_.push_back( + new QueuedNotification(notification, profile)); + CheckAndShowNotifications(); +} + +bool NotificationUIManager::Cancel(const Notification& notification) { + // First look through the notifications that haven't been shown. If not + // found there, call to the active balloon collection to tear it down. + NotificationDeque::iterator iter; + for (iter = show_queue_.begin(); iter != show_queue_.end(); ++iter) { + if (notification.IsSame((*iter)->notification())) { + show_queue_.erase(iter); + return true; + } + } + return balloon_collection_->Remove(notification); +} + +void NotificationUIManager::CheckAndShowNotifications() { + // TODO(johnnyg): http://crbug.com/25061 - Check for user idle/presentation. + ShowNotifications(); +} + +void NotificationUIManager::ShowNotifications() { + while (!show_queue_.empty() && balloon_collection_->HasSpace()) { + scoped_ptr<QueuedNotification> queued_notification(show_queue_.front()); + show_queue_.pop_front(); + balloon_collection_->Add(queued_notification->notification(), + queued_notification->profile()); + } +} + +void NotificationUIManager::OnBalloonSpaceChanged() { + CheckAndShowNotifications(); +} + +bool NotificationUIManager::TryReplacement(const Notification& notification) { + const GURL& origin = notification.origin_url(); + const string16& replace_id = notification.replace_id(); + + if (replace_id.empty()) + return false; + + // First check the queue of pending notifications for replacement. + // Then check the list of notifications already being shown. + NotificationDeque::iterator iter; + for (iter = show_queue_.begin(); iter != show_queue_.end(); ++iter) { + if (origin == (*iter)->notification().origin_url() && + replace_id == (*iter)->notification().replace_id()) { + (*iter)->Replace(notification); + return true; + } + } + + BalloonCollection::Balloons::iterator balloon_iter; + BalloonCollection::Balloons balloons = + balloon_collection_->GetActiveBalloons(); + for (balloon_iter = balloons.begin(); + balloon_iter != balloons.end(); + ++balloon_iter) { + if (origin == (*balloon_iter)->notification().origin_url() && + replace_id == (*balloon_iter)->notification().replace_id()) { + (*balloon_iter)->Update(notification); + return true; + } + } + + return false; +} diff --git a/chrome/browser/notifications/notification_ui_manager.h b/chrome/browser/notifications/notification_ui_manager.h new file mode 100644 index 0000000..e510e7f --- /dev/null +++ b/chrome/browser/notifications/notification_ui_manager.h @@ -0,0 +1,78 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_UI_MANAGER_H_ +#define CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_UI_MANAGER_H_ + +#include <deque> + +#include "base/id_map.h" +#include "base/scoped_ptr.h" +#include "chrome/browser/notifications/balloon.h" +#include "chrome/browser/notifications/balloon_collection.h" + +class Notification; +class Profile; +class QueuedNotification; +class SiteInstance; + +// The notification manager manages use of the desktop for notifications. +// It maintains a queue of pending notifications when space becomes constrained. +class NotificationUIManager + : public BalloonCollection::BalloonSpaceChangeListener { + public: + NotificationUIManager(); + virtual ~NotificationUIManager(); + + // Creates an initialized UI manager with a new balloon collection + // and the listener relationship setup. + // Except for unit tests, this is the way to construct the object. + static NotificationUIManager* Create(); + + // Initializes the UI manager with a balloon collection; this object + // takes ownership of the balloon collection. + void Initialize(BalloonCollection* balloon_collection) { + DCHECK(!balloon_collection_.get()); + DCHECK(balloon_collection); + balloon_collection_.reset(balloon_collection); + } + + // Adds a notification to be displayed. Virtual for unit test override. + virtual void Add(const Notification& notification, + Profile* profile); + + // Removes a notification. + virtual bool Cancel(const Notification& notification); + + // Returns balloon collection. + BalloonCollection* balloon_collection() { + return balloon_collection_.get(); + } + + private: + // Attempts to display notifications from the show_queue if the user + // is active. + void CheckAndShowNotifications(); + + // Attempts to display notifications from the show_queue. + void ShowNotifications(); + + // BalloonCollectionObserver implementation. + virtual void OnBalloonSpaceChanged(); + + // Replace an existing notification with this one if applicable; + // returns true if the replacement happened. + bool TryReplacement(const Notification& notification); + + // An owned pointer to the collection of active balloons. + scoped_ptr<BalloonCollection> balloon_collection_; + + // A queue of notifications which are waiting to be shown. + typedef std::deque<QueuedNotification*> NotificationDeque; + NotificationDeque show_queue_; + + DISALLOW_COPY_AND_ASSIGN(NotificationUIManager); +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_UI_MANAGER_H_ diff --git a/chrome/browser/notifications/notifications_interactive_uitest.cc b/chrome/browser/notifications/notifications_interactive_uitest.cc new file mode 100644 index 0000000..d8e4b27 --- /dev/null +++ b/chrome/browser/notifications/notifications_interactive_uitest.cc @@ -0,0 +1,69 @@ +// Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/file_path.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/test/automation/browser_proxy.h" +#include "chrome/test/automation/tab_proxy.h" +#include "chrome/test/ui/ui_test.h" +#include "net/base/net_util.h" +#include "net/url_request/url_request_unittest.h" + +class NotificationsPermissionTest : public UITest { + public: + NotificationsPermissionTest() { + dom_automation_enabled_ = true; + show_window_ = true; + } +}; + +TEST_F(NotificationsPermissionTest, TestUserGestureInfobar) { + const wchar_t kDocRoot[] = L"chrome/test/data"; + scoped_refptr<HTTPTestServer> server = + HTTPTestServer::CreateServer(kDocRoot, NULL); + ASSERT_TRUE(server.get() != NULL); + + scoped_refptr<BrowserProxy> browser(automation()->GetBrowserWindow(0)); + ASSERT_TRUE(browser.get()); + scoped_refptr<TabProxy> tab(browser->GetActiveTab()); + ASSERT_TRUE(tab.get()); + ASSERT_EQ(AUTOMATION_MSG_NAVIGATION_SUCCESS, + tab->NavigateToURL(server->TestServerPage( + "files/notifications/notifications_request_function.html"))); + WaitUntilTabCount(1); + + // Request permission by calling request() while eval'ing an inline script; + // That's considered a user gesture to webkit, and should produce an infobar. + bool result; + ASSERT_TRUE(tab->ExecuteAndExtractBool( + L"", + L"window.domAutomationController.send(request());", + &result)); + EXPECT_TRUE(result); + + EXPECT_TRUE(tab->WaitForInfoBarCount(1, action_max_timeout_ms())); +} + +TEST_F(NotificationsPermissionTest, TestNoUserGestureInfobar) { + const wchar_t kDocRoot[] = L"chrome/test/data"; + scoped_refptr<HTTPTestServer> server = + HTTPTestServer::CreateServer(kDocRoot, NULL); + ASSERT_TRUE(server.get() != NULL); + + scoped_refptr<BrowserProxy> browser(automation()->GetBrowserWindow(0)); + ASSERT_TRUE(browser.get()); + scoped_refptr<TabProxy> tab(browser->GetActiveTab()); + ASSERT_TRUE(tab.get()); + + // Load a page which just does a request; no user gesture should result + // in no infobar. + ASSERT_EQ(AUTOMATION_MSG_NAVIGATION_SUCCESS, + tab->NavigateToURL(server->TestServerPage( + "files/notifications/notifications_request_inline.html"))); + WaitUntilTabCount(1); + + int info_bar_count; + ASSERT_TRUE(tab->GetInfoBarCount(&info_bar_count)); + EXPECT_EQ(0, info_bar_count); +} diff --git a/chrome/browser/notifications/notifications_prefs_cache.cc b/chrome/browser/notifications/notifications_prefs_cache.cc new file mode 100644 index 0000000..2fec1e8 --- /dev/null +++ b/chrome/browser/notifications/notifications_prefs_cache.cc @@ -0,0 +1,100 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/notifications_prefs_cache.h" + +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/pref_service.h" +#include "third_party/WebKit/WebKit/chromium/public/WebNotificationPresenter.h" + +NotificationsPrefsCache::NotificationsPrefsCache() + : default_content_setting_(CONTENT_SETTING_DEFAULT), + is_initialized_(false) { +} + +void NotificationsPrefsCache::CacheAllowedOrigin( + const GURL& origin) { + CheckThreadAccess(); + std::set<GURL>::iterator iter; + allowed_origins_.insert(origin); + if ((iter = denied_origins_.find(origin)) != denied_origins_.end()) + denied_origins_.erase(iter); +} + +void NotificationsPrefsCache::CacheDeniedOrigin( + const GURL& origin) { + CheckThreadAccess(); + std::set<GURL>::iterator iter; + denied_origins_.insert(origin); + if ((iter = allowed_origins_.find(origin)) != allowed_origins_.end()) + allowed_origins_.erase(iter); +} + +void NotificationsPrefsCache::SetCacheAllowedOrigins( + const std::vector<GURL>& allowed) { + allowed_origins_.clear(); + allowed_origins_.insert(allowed.begin(), allowed.end()); +} + +void NotificationsPrefsCache::SetCacheDeniedOrigins( + const std::vector<GURL>& denied) { + denied_origins_.clear(); + denied_origins_.insert(denied.begin(), denied.end()); +} + +void NotificationsPrefsCache::SetCacheDefaultContentSetting( + ContentSetting setting) { + default_content_setting_ = setting; +} + +// static +void NotificationsPrefsCache::ListValueToGurlVector( + const ListValue& origin_list, + std::vector<GURL>* origin_vector) { + ListValue::const_iterator i; + std::wstring origin; + for (i = origin_list.begin(); i != origin_list.end(); ++i) { + (*i)->GetAsString(&origin); + origin_vector->push_back(GURL(WideToUTF8(origin))); + } +} + +int NotificationsPrefsCache::HasPermission(const GURL& origin) { + if (IsOriginAllowed(origin)) + return WebKit::WebNotificationPresenter::PermissionAllowed; + if (IsOriginDenied(origin)) + return WebKit::WebNotificationPresenter::PermissionDenied; + switch (default_content_setting_) { + case CONTENT_SETTING_ALLOW: + return WebKit::WebNotificationPresenter::PermissionAllowed; + case CONTENT_SETTING_BLOCK: + return WebKit::WebNotificationPresenter::PermissionDenied; + case CONTENT_SETTING_ASK: + case CONTENT_SETTING_DEFAULT: + default: // Make gcc happy. + return WebKit::WebNotificationPresenter::PermissionNotAllowed; + } +} + +bool NotificationsPrefsCache::IsOriginAllowed( + const GURL& origin) { + CheckThreadAccess(); + return allowed_origins_.find(origin) != allowed_origins_.end(); +} + +bool NotificationsPrefsCache::IsOriginDenied( + const GURL& origin) { + CheckThreadAccess(); + return denied_origins_.find(origin) != denied_origins_.end(); +} + +void NotificationsPrefsCache::CheckThreadAccess() { + if (is_initialized_) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO)); + } else { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + } +} diff --git a/chrome/browser/notifications/notifications_prefs_cache.h b/chrome/browser/notifications/notifications_prefs_cache.h new file mode 100644 index 0000000..61bb983 --- /dev/null +++ b/chrome/browser/notifications/notifications_prefs_cache.h @@ -0,0 +1,81 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_NOTIFICATIONS_NOTIFICATIONS_PREFS_CACHE_H_ +#define CHROME_BROWSER_NOTIFICATIONS_NOTIFICATIONS_PREFS_CACHE_H_ + +#include <set> +#include <vector> + +#include "base/ref_counted.h" +#include "chrome/common/content_settings.h" +#include "googleurl/src/gurl.h" + +class ListValue; + +// Class which caches notification preferences. +// Construction occurs on the UI thread when the contents +// of the profile preferences are initialized. Once is_initialized() is set, +// access can only be done from the IO thread. +class NotificationsPrefsCache + : public base::RefCountedThreadSafe<NotificationsPrefsCache> { + public: + NotificationsPrefsCache(); + + // Once is_initialized() is set, all accesses must happen on the IO thread. + // Before that, all accesses need to happen on the UI thread. + void set_is_initialized(bool val) { is_initialized_ = val; } + bool is_initialized() { return is_initialized_; } + + // Checks to see if a given origin has permission to create desktop + // notifications. Returns a constant from WebNotificationPresenter + // class. + int HasPermission(const GURL& origin); + + // Updates the cache with a new origin allowed or denied. + void CacheAllowedOrigin(const GURL& origin); + void CacheDeniedOrigin(const GURL& origin); + + // Set the cache to the supplied values. This clears the current + // contents of the cache. + void SetCacheAllowedOrigins(const std::vector<GURL>& allowed); + void SetCacheDeniedOrigins(const std::vector<GURL>& denied); + void SetCacheDefaultContentSetting(ContentSetting setting); + + static void ListValueToGurlVector(const ListValue& origin_list, + std::vector<GURL>* origin_vector); + + // Exposed for testing. + ContentSetting CachedDefaultContentSetting() { + return default_content_setting_; + } + + private: + friend class base::RefCountedThreadSafe<NotificationsPrefsCache>; + + ~NotificationsPrefsCache() {} + + // Helper functions which read preferences. + bool IsOriginAllowed(const GURL& origin); + bool IsOriginDenied(const GURL& origin); + + // Helper that ensures we are running on the expected thread. + void CheckThreadAccess(); + + // Storage of the actual preferences. + std::set<GURL> allowed_origins_; + std::set<GURL> denied_origins_; + + // The default setting, used for origins that are neither in + // |allowed_origins_| nor |denied_origins_|. + ContentSetting default_content_setting_; + + // Set to true once the initial cached settings have been completely read. + // Once this is done, the class can no longer be accessed on the UI thread. + bool is_initialized_; + + DISALLOW_COPY_AND_ASSIGN(NotificationsPrefsCache); +}; + +#endif // CHROME_BROWSER_NOTIFICATIONS_NOTIFICATIONS_PREFS_CACHE_H_ diff --git a/chrome/browser/notifications/notifications_prefs_cache_unittest.cc b/chrome/browser/notifications/notifications_prefs_cache_unittest.cc new file mode 100644 index 0000000..0cbea9e --- /dev/null +++ b/chrome/browser/notifications/notifications_prefs_cache_unittest.cc @@ -0,0 +1,63 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/notifications/notifications_prefs_cache.h" + +#include "base/message_loop.h" +#include "chrome/browser/chrome_thread.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/WebKit/WebKit/chromium/public/WebNotificationPresenter.h" + +TEST(NotificationsPrefsCacheTest, CanCreate) { + scoped_refptr<NotificationsPrefsCache> cache(new NotificationsPrefsCache()); + std::vector<GURL> allowed_origins; + allowed_origins.push_back(GURL("http://allowed.com")); + std::vector<GURL> denied_origins; + denied_origins.push_back(GURL("http://denied.com")); + + { + MessageLoop loop; + ChromeThread ui_thread(ChromeThread::UI, &loop); + + cache->SetCacheAllowedOrigins(allowed_origins); + cache->SetCacheDeniedOrigins(denied_origins); + cache->SetCacheDefaultContentSetting(CONTENT_SETTING_DEFAULT); + } + + cache->set_is_initialized(true); + + { + MessageLoop loop; + ChromeThread io_thread(ChromeThread::IO, &loop); + + cache->CacheAllowedOrigin(GURL("http://allowed2.com")); + cache->CacheDeniedOrigin(GURL("http://denied2.com")); + + EXPECT_EQ(cache->HasPermission(GURL("http://allowed.com")), + WebKit::WebNotificationPresenter::PermissionAllowed); + EXPECT_EQ(cache->HasPermission(GURL("http://allowed2.com")), + WebKit::WebNotificationPresenter::PermissionAllowed); + + EXPECT_EQ(cache->HasPermission(GURL("http://denied.com")), + WebKit::WebNotificationPresenter::PermissionDenied); + EXPECT_EQ(cache->HasPermission(GURL("http://denied2.com")), + WebKit::WebNotificationPresenter::PermissionDenied); + + EXPECT_EQ(cache->HasPermission(GURL("http://unkown.com")), + WebKit::WebNotificationPresenter::PermissionNotAllowed); + + cache->SetCacheDefaultContentSetting(CONTENT_SETTING_ASK); + EXPECT_EQ(cache->HasPermission(GURL("http://unkown.com")), + WebKit::WebNotificationPresenter::PermissionNotAllowed); + + cache->SetCacheDefaultContentSetting(CONTENT_SETTING_ALLOW); + EXPECT_EQ(cache->HasPermission(GURL("http://unkown.com")), + WebKit::WebNotificationPresenter::PermissionAllowed); + + cache->SetCacheDefaultContentSetting(CONTENT_SETTING_BLOCK); + EXPECT_EQ(cache->HasPermission(GURL("http://unkown.com")), + WebKit::WebNotificationPresenter::PermissionDenied); + } +} + |