Commit 1d535191 authored by Jay Harris's avatar Jay Harris Committed by Commit Bot

Adds support for badging scopes.

There has been significant discussion on the explainer repo
about the best way for badging to work. The API has undergone
a major redesign.

Previously, badges were associated with a specific installed
web application, which severely limited the usefulness of the
API.

In the revised API, a badge is associated with a scope. This
reflects the way that web applications work, so it is possible
to badge multiple nested applications. In addition, this
should allow us to expose the API from the service worker in
future (which is not implicitly associated with any
application). The scopes solution has the added advantage
that other, non-app contexts (such as bookmarks) could be
badged in the future.

Explainer:
https://github.com/WICG/badging/blob/master/explainer.md

Bug: 987514
Change-Id: Ib11500e1b4f26eabe402153d27176e786c33768a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1723849
Commit-Queue: Matt Giuca <mgiuca@chromium.org>
Reviewed-by: default avatarMatt Giuca <mgiuca@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Cr-Commit-Position: refs/heads/master@{#692412}
parent 4805b398
...@@ -2994,6 +2994,7 @@ jumbo_split_static_library("browser") { ...@@ -2994,6 +2994,7 @@ jumbo_split_static_library("browser") {
"background/background_contents_service_observer.h", "background/background_contents_service_observer.h",
"badging/badge_manager.cc", "badging/badge_manager.cc",
"badging/badge_manager.h", "badging/badge_manager.h",
"badging/badge_manager_delegate.cc",
"badging/badge_manager_delegate.h", "badging/badge_manager_delegate.h",
"badging/badge_manager_factory.cc", "badging/badge_manager_factory.cc",
"badging/badge_manager_factory.h", "badging/badge_manager_factory.h",
......
...@@ -9,11 +9,13 @@ ...@@ -9,11 +9,13 @@
#include "base/i18n/number_formatting.h" #include "base/i18n/number_formatting.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "build/build_config.h" #include "build/build_config.h"
#include "chrome/browser/badging/badge_manager_delegate.h"
#include "chrome/browser/badging/badge_manager_factory.h" #include "chrome/browser/badging/badge_manager_factory.h"
#include "chrome/browser/profiles/profile.h" #include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h" #include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h" #include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/web_applications/components/web_app_provider_base.h"
#include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h" #include "content/public/browser/render_process_host.h"
#include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util.h"
...@@ -27,23 +29,15 @@ ...@@ -27,23 +29,15 @@
namespace badging { namespace badging {
std::string GetBadgeString(base::Optional<uint64_t> badge_content) {
if (!badge_content)
return "•";
if (badge_content > kMaxBadgeContent) {
return base::UTF16ToUTF8(l10n_util::GetStringFUTF16(
IDS_SATURATED_BADGE_CONTENT, base::FormatNumber(kMaxBadgeContent)));
}
return base::UTF16ToUTF8(base::FormatNumber(badge_content.value()));
}
BadgeManager::BadgeManager(Profile* profile) { BadgeManager::BadgeManager(Profile* profile) {
#if defined(OS_MACOSX) #if defined(OS_MACOSX)
SetDelegate(std::make_unique<BadgeManagerDelegateMac>(profile)); SetDelegate(std::make_unique<BadgeManagerDelegateMac>(
profile, this,
&web_app::WebAppProviderBase::GetProviderBase(profile)->registrar()));
#elif defined(OS_WIN) #elif defined(OS_WIN)
SetDelegate(std::make_unique<BadgeManagerDelegateWin>(profile)); SetDelegate(std::make_unique<BadgeManagerDelegateWin>(
profile, this,
&web_app::WebAppProviderBase::GetProviderBase(profile)->registrar()));
#endif #endif
} }
...@@ -66,79 +60,107 @@ void BadgeManager::BindRequest( ...@@ -66,79 +60,107 @@ void BadgeManager::BindRequest(
std::move(context)); std::move(context));
} }
void BadgeManager::UpdateAppBadge(const base::Optional<std::string>& app_id, bool BadgeManager::HasMoreSpecificBadgeForUrl(const GURL& scope,
base::Optional<uint64_t> content) { const GURL& url) {
// Badge content should never be 0 (it should be translated into a clear). return MostSpecificBadgeForScope(url).spec().size() > scope.spec().size();
DCHECK_NE(content.value_or(1), 0u); }
if (!app_id) { base::Optional<BadgeManager::BadgeValue> BadgeManager::GetBadgeValue(
BadgeChangeIgnored(); const GURL& scope) {
return; const GURL& most_specific = MostSpecificBadgeForScope(scope);
} if (most_specific == GURL::EmptyGURL())
return base::nullopt;
badged_apps_[app_id.value()] = content; return base::make_optional(badged_scopes_[most_specific]);
}
void BadgeManager::SetBadgeForTesting(const GURL& scope, BadgeValue value) {
UpdateBadge(scope, value);
}
void BadgeManager::ClearBadgeForTesting(const GURL& scope) {
UpdateBadge(scope, base::nullopt);
}
void BadgeManager::UpdateBadge(const GURL& scope,
base::Optional<BadgeValue> value) {
if (!value)
badged_scopes_.erase(scope);
else
badged_scopes_[scope] = value.value();
if (!delegate_) if (!delegate_)
return; return;
delegate_->OnBadgeSet(app_id.value(), content); delegate_->OnBadgeUpdated(scope);
} }
void BadgeManager::ClearAppBadge(const base::Optional<std::string>& app_id) { void BadgeManager::SetBadge(const GURL& scope,
if (!app_id) { blink::mojom::BadgeValuePtr mojo_value) {
BadgeChangeIgnored(); if (mojo_value->is_number() && mojo_value->get_number() == 0) {
mojo::ReportBadMessage(
"|value| should not be zero when it is |number| (ClearBadge should be "
"called instead)!");
return; return;
} }
badged_apps_.erase(app_id.value()); if (!ScopeIsValidBadgeTarget(receivers_.current_context(), scope))
if (!delegate_)
return; return;
delegate_->OnBadgeCleared(app_id.value()); // Convert the mojo badge representation into a BadgeManager::BadgeValue.
BadgeValue value = mojo_value->is_flag()
? base::nullopt
: base::make_optional(mojo_value->get_number());
UpdateBadge(scope, base::make_optional(value));
} }
void BadgeManager::BadgeChangeIgnored() { void BadgeManager::ClearBadge(const GURL& scope) {
if (!delegate_) if (!ScopeIsValidBadgeTarget(receivers_.current_context(), scope))
return; return;
delegate_->OnBadgeChangeIgnoredForTesting(); UpdateBadge(scope, base::nullopt);
} }
void BadgeManager::SetInteger(uint64_t content) { GURL BadgeManager::MostSpecificBadgeForScope(const GURL& scope) {
UpdateAppBadge(GetAppIdToBadge(receivers_.current_context()), content); const std::string& scope_string = scope.spec();
} GURL best_match = GURL::EmptyGURL();
uint64_t longest_match = 0;
void BadgeManager::SetFlag() { for (const auto& pair : badged_scopes_) {
UpdateAppBadge(GetAppIdToBadge(receivers_.current_context()), base::nullopt); const std::string& cur_scope_str = pair.first.spec();
} if (scope_string.find(cur_scope_str) != 0)
continue;
void BadgeManager::ClearBadge() { if (longest_match >= cur_scope_str.size())
ClearAppBadge(GetAppIdToBadge(receivers_.current_context())); continue;
longest_match = cur_scope_str.size();
best_match = pair.first;
}
return best_match;
} }
base::Optional<std::string> BadgeManager::GetAppIdToBadge( bool BadgeManager::ScopeIsValidBadgeTarget(const BindingContext& context,
const BindingContext& context) { const GURL& scope) {
content::RenderFrameHost* frame = content::RenderFrameHost* frame =
content::RenderFrameHost::FromID(context.process_id, context.frame_id); content::RenderFrameHost::FromID(context.process_id, context.frame_id);
if (!frame) if (!frame)
return base::nullopt; return false;
content::WebContents* contents = return url::IsSameOriginWith(frame->GetLastCommittedURL(), scope);
content::WebContents::FromRenderFrameHost(frame); }
Browser* browser = chrome::FindBrowserWithWebContents(contents);
if (!browser)
return base::nullopt;
web_app::AppBrowserController* app_controller = browser->app_controller(); std::string GetBadgeString(base::Optional<uint64_t> badge_content) {
if (!app_controller) if (!badge_content)
return base::nullopt; return "•";
// If the frame is not in scope, don't apply a badge. if (badge_content > kMaxBadgeContent) {
if (!app_controller->IsUrlInAppScope(frame->GetLastCommittedURL())) { return base::UTF16ToUTF8(l10n_util::GetStringFUTF16(
return base::nullopt; IDS_SATURATED_BADGE_CONTENT, base::FormatNumber(kMaxBadgeContent)));
} }
return app_controller->GetAppId(); return base::UTF16ToUTF8(base::FormatNumber(badge_content.value()));
} }
} // namespace badging } // namespace badging
...@@ -11,10 +11,10 @@ ...@@ -11,10 +11,10 @@
#include "base/macros.h" #include "base/macros.h"
#include "base/optional.h" #include "base/optional.h"
#include "chrome/browser/badging/badge_manager_delegate.h"
#include "components/keyed_service/core/keyed_service.h" #include "components/keyed_service/core/keyed_service.h"
#include "mojo/public/cpp/bindings/receiver_set.h" #include "mojo/public/cpp/bindings/receiver_set.h"
#include "third_party/blink/public/mojom/badging/badging.mojom.h" #include "third_party/blink/public/mojom/badging/badging.mojom.h"
#include "url/gurl.h"
class Profile; class Profile;
...@@ -23,17 +23,19 @@ class RenderFrameHost; ...@@ -23,17 +23,19 @@ class RenderFrameHost;
} // namespace content } // namespace content
namespace badging { namespace badging {
class BadgeManagerDelegate;
// The maximum value of badge contents before saturation occurs. // The maximum value of badge contents before saturation occurs.
constexpr uint64_t kMaxBadgeContent = 99u; constexpr uint64_t kMaxBadgeContent = 99u;
// Determines the text to put on the badge based on some badge_content.
std::string GetBadgeString(base::Optional<uint64_t> badge_content);
// Maintains a record of badge contents and dispatches badge changes to a // Maintains a record of badge contents and dispatches badge changes to a
// delegate. // delegate.
class BadgeManager : public KeyedService, public blink::mojom::BadgeService { class BadgeManager : public KeyedService, public blink::mojom::BadgeService {
public: public:
// The badge being applied to a URL. If the optional is |base::nullopt| then
// the badge is "flag". Otherwise the badge is a non-zero integer.
using BadgeValue = base::Optional<uint64_t>;
explicit BadgeManager(Profile* profile); explicit BadgeManager(Profile* profile);
~BadgeManager() override; ~BadgeManager() override;
...@@ -44,13 +46,16 @@ class BadgeManager : public KeyedService, public blink::mojom::BadgeService { ...@@ -44,13 +46,16 @@ class BadgeManager : public KeyedService, public blink::mojom::BadgeService {
mojo::PendingReceiver<blink::mojom::BadgeService> receiver, mojo::PendingReceiver<blink::mojom::BadgeService> receiver,
content::RenderFrameHost* frame); content::RenderFrameHost* frame);
// Sets the badge for |app_id| to be |content|. Note: If content is set, it // Returns whether there is a more specific badge for |url| than |scope|.
// must be non-zero. // Note: This function does not check that there is a badge for |scope|.
void UpdateAppBadge(const base::Optional<std::string>& app_id, bool HasMoreSpecificBadgeForUrl(const GURL& scope, const GURL& url);
base::Optional<uint64_t> content);
// Gets the most specific badge applying to |scope|. This will be
// base::nullopt if the scope is not badged.
base::Optional<BadgeValue> GetBadgeValue(const GURL& scope);
// Clears the badge for |app_id|. void SetBadgeForTesting(const GURL& scope, BadgeValue value);
void ClearAppBadge(const base::Optional<std::string>& app_id); void ClearBadgeForTesting(const GURL& scope);
private: private:
// The BindingContext of a mojo request. Allows mojo calls to be tied back to // The BindingContext of a mojo request. Allows mojo calls to be tied back to
...@@ -63,18 +68,23 @@ class BadgeManager : public KeyedService, public blink::mojom::BadgeService { ...@@ -63,18 +68,23 @@ class BadgeManager : public KeyedService, public blink::mojom::BadgeService {
int frame_id; int frame_id;
}; };
// Notifies |delegate_| that a badge change was ignored. // Updates the badge for |scope| to be |value|, if it is not base::nullopt.
void BadgeChangeIgnored(); // If value is |base::nullopt| then this clears the badge.
void UpdateBadge(const GURL& scope, base::Optional<BadgeValue> value);
// blink::mojom::BadgeService: // blink::mojom::BadgeService:
// Note: These are private to stop them being called outside of mojo as they // Note: These are private to stop them being called outside of mojo as they
// require a mojo binding context. // require a mojo binding context.
void SetInteger(uint64_t content) override; void SetBadge(const GURL& scope, blink::mojom::BadgeValuePtr value) override;
void SetFlag() override; void ClearBadge(const GURL& scope) override;
void ClearBadge() override;
// Examines |context| to determine which app, if any, should be badged. // Finds the scope URL of the most specific badge for |scope|. Returns
base::Optional<std::string> GetAppIdToBadge(const BindingContext& context); // GURL::EmptyGURL() if no match is found.
GURL MostSpecificBadgeForScope(const GURL& scope);
// Determines whether |context| is allowed to change the badge for |scope|.
bool ScopeIsValidBadgeTarget(const BindingContext& context,
const GURL& scope);
// All the mojo receivers for the BadgeManager. Keeps track of the // All the mojo receivers for the BadgeManager. Keeps track of the
// render_frame the binding is associated with, so as to not have to rely // render_frame the binding is associated with, so as to not have to rely
...@@ -85,12 +95,15 @@ class BadgeManager : public KeyedService, public blink::mojom::BadgeService { ...@@ -85,12 +95,15 @@ class BadgeManager : public KeyedService, public blink::mojom::BadgeService {
// Note: This is currently only set on Windows and MacOS. // Note: This is currently only set on Windows and MacOS.
std::unique_ptr<BadgeManagerDelegate> delegate_; std::unique_ptr<BadgeManagerDelegate> delegate_;
// Maps app id to badge contents. // Maps scope to badge contents.
std::map<std::string, base::Optional<uint64_t>> badged_apps_; std::map<GURL, BadgeValue> badged_scopes_;
DISALLOW_COPY_AND_ASSIGN(BadgeManager); DISALLOW_COPY_AND_ASSIGN(BadgeManager);
}; };
// Determines the text to put on the badge based on some badge_content.
std::string GetBadgeString(BadgeManager::BadgeValue badge_content);
} // namespace badging } // namespace badging
#endif // CHROME_BROWSER_BADGING_BADGE_MANAGER_H_ #endif // CHROME_BROWSER_BADGING_BADGE_MANAGER_H_
// Copyright 2019 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/badging/badge_manager_delegate.h"
#include <vector>
#include "chrome/browser/web_applications/components/app_registrar.h"
namespace badging {
void BadgeManagerDelegate::OnBadgeUpdated(const GURL& scope) {
const std::vector<web_app::AppId>& app_ids =
registrar()->FindAppsInScope(scope);
for (const auto& app_id : app_ids) {
const auto& app_scope = registrar()->GetAppScope(app_id);
if (!app_scope)
continue;
// If it wasn't the most specific badge for the app that changed, there's no
// need to update it.
if (badge_manager()->HasMoreSpecificBadgeForUrl(scope, app_scope.value()))
continue;
OnAppBadgeUpdated(app_id);
}
}
base::Optional<BadgeManager::BadgeValue> BadgeManagerDelegate::GetAppBadgeValue(
const web_app::AppId& app_id) {
const auto& scope = registrar()->GetAppScope(app_id);
if (!scope)
return base::nullopt;
return badge_manager()->GetBadgeValue(scope.value());
}
} // namespace badging
...@@ -5,36 +5,56 @@ ...@@ -5,36 +5,56 @@
#ifndef CHROME_BROWSER_BADGING_BADGE_MANAGER_DELEGATE_H_ #ifndef CHROME_BROWSER_BADGING_BADGE_MANAGER_DELEGATE_H_
#define CHROME_BROWSER_BADGING_BADGE_MANAGER_DELEGATE_H_ #define CHROME_BROWSER_BADGING_BADGE_MANAGER_DELEGATE_H_
#include <string>
#include "base/optional.h" #include "base/optional.h"
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "url/gurl.h"
class Profile; class Profile;
namespace web_app {
class AppRegistrar;
}
namespace badging { namespace badging {
// BadgeManagerDelegate is responsible for dispatching badge events that should // A BadgeManagerDelegate is responsible for updating the UI in response to a
// be handled and reflected in the UI. // badge change.
class BadgeManagerDelegate { class BadgeManagerDelegate {
public: public:
explicit BadgeManagerDelegate(Profile* profile) : profile_(profile) {} explicit BadgeManagerDelegate(Profile* profile,
BadgeManager* badge_manager,
web_app::AppRegistrar* registrar)
: profile_(profile),
badge_manager_(badge_manager),
registrar_(registrar) {}
virtual ~BadgeManagerDelegate() = default;
virtual ~BadgeManagerDelegate() {} // Called when the badge for |scope| has changed.
virtual void OnBadgeUpdated(const GURL& scope);
// Called when an app's badge has changed. protected:
virtual void OnBadgeSet(const std::string& app_id, // Called when the badge for |app_id| has changed.
base::Optional<uint64_t> contents) = 0; virtual void OnAppBadgeUpdated(const web_app::AppId& app_id) = 0;
// Called when a app's badge has been cleared. // Gets the badge for |app_id|. base::nullopt if the |app_id| is not badged.
virtual void OnBadgeCleared(const std::string& app_id) = 0; base::Optional<BadgeManager::BadgeValue> GetAppBadgeValue(
const web_app::AppId& app_id);
// Called when a page attempts to set or clear a badge but the badge service Profile* profile() { return profile_; }
// determines that the page should not able to change the badge. BadgeManager* badge_manager() { return badge_manager_; }
virtual void OnBadgeChangeIgnoredForTesting() {} web_app::AppRegistrar* registrar() { return registrar_; }
protected: private:
// The profile the badge manager delegate is associated with. // The profile the badge manager delegate is associated with.
Profile* profile_; Profile* profile_;
// The badge manager that owns this delegate.
BadgeManager* badge_manager_;
// The registrar of apps this delegate is concerned with.
web_app::AppRegistrar* registrar_;
DISALLOW_COPY_AND_ASSIGN(BadgeManagerDelegate);
}; };
} // namespace badging } // namespace badging
......
...@@ -9,22 +9,21 @@ ...@@ -9,22 +9,21 @@
#include "chrome/browser/badging/badge_manager.h" #include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/profiles/profile.h" #include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_list.h" #include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
#include "chrome/common/mac/app_shim.mojom.h" #include "chrome/common/mac/app_shim.mojom.h"
namespace {} // namespace
namespace badging { namespace badging {
BadgeManagerDelegateMac::BadgeManagerDelegateMac(Profile* profile) BadgeManagerDelegateMac::BadgeManagerDelegateMac(
: BadgeManagerDelegate(profile) {} Profile* profile,
BadgeManager* badge_manager,
void BadgeManagerDelegateMac::OnBadgeSet(const std::string& app_id, web_app::AppRegistrar* registrar)
base::Optional<uint64_t> contents) { : BadgeManagerDelegate(profile, badge_manager, registrar) {}
SetAppBadgeLabel(app_id, badging::GetBadgeString(contents));
}
void BadgeManagerDelegateMac::OnBadgeCleared(const std::string& app_id) { void BadgeManagerDelegateMac::OnAppBadgeUpdated(const web_app::AppId& app_id) {
SetAppBadgeLabel(app_id, ""); const base::Optional<BadgeManager::BadgeValue>& badge =
GetAppBadgeValue(app_id);
SetAppBadgeLabel(app_id, badge ? badging::GetBadgeString(badge.value()) : "");
} }
void BadgeManagerDelegateMac::SetAppBadgeLabel(const std::string& app_id, void BadgeManagerDelegateMac::SetAppBadgeLabel(const std::string& app_id,
...@@ -33,9 +32,9 @@ void BadgeManagerDelegateMac::SetAppBadgeLabel(const std::string& app_id, ...@@ -33,9 +32,9 @@ void BadgeManagerDelegateMac::SetAppBadgeLabel(const std::string& app_id,
if (!shim_handler) if (!shim_handler)
return; return;
// On OSX all app instances share a dock icon, so we only need to set the // On macOS all app instances share a dock icon, so we only need to set the
// badge label once. // badge label once.
AppShimHost* shim_host = shim_handler->FindHost(profile_, app_id); AppShimHost* shim_host = shim_handler->FindHost(profile(), app_id);
if (!shim_host) if (!shim_host)
return; return;
......
...@@ -12,17 +12,22 @@ ...@@ -12,17 +12,22 @@
class Profile; class Profile;
namespace web_app {
class AppRegistrar;
}
namespace badging { namespace badging {
class BadgeManager;
// OSX specific implementation of the BadgeManagerDelegate. // OSX specific implementation of the BadgeManagerDelegate.
class BadgeManagerDelegateMac : public BadgeManagerDelegate { class BadgeManagerDelegateMac : public BadgeManagerDelegate {
public: public:
explicit BadgeManagerDelegateMac(Profile* profile); explicit BadgeManagerDelegateMac(Profile* profile,
BadgeManager* badge_manager,
void OnBadgeSet(const std::string& app_id, web_app::AppRegistrar* registrar);
base::Optional<uint64_t> contents) override;
void OnBadgeCleared(const std::string& app_id) override; void OnAppBadgeUpdated(const web_app::AppId& app_id) override;
private: private:
void SetAppBadgeLabel(const std::string& app_id, void SetAppBadgeLabel(const std::string& app_id,
......
// Copyright 2019 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 <memory>
#include <utility>
#include "base/optional.h"
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/badging/badge_manager_factory.h"
#include "chrome/browser/badging/test_badge_manager_delegate.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/test/test_app_registrar.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace badging {
class BadgeManagerDelegateUnittest : public ::testing::Test {
public:
BadgeManagerDelegateUnittest() = default;
~BadgeManagerDelegateUnittest() override = default;
void SetUp() override {
profile_.reset(new TestingProfile());
registrar_.reset(new web_app::TestAppRegistrar());
badge_manager_ =
BadgeManagerFactory::GetInstance()->GetForProfile(profile_.get());
// Delegate lifetime is managed by BadgeManager.
auto owned_delegate = std::make_unique<TestBadgeManagerDelegate>(
profile_.get(), badge_manager_, registrar());
delegate_ = owned_delegate.get();
badge_manager_->SetDelegate(std::move(owned_delegate));
}
void TearDown() override { profile_.reset(); }
TestBadgeManagerDelegate* delegate() { return delegate_; }
web_app::TestAppRegistrar* registrar() const { return registrar_.get(); }
BadgeManager* badge_manager() const { return badge_manager_; }
private:
std::unique_ptr<web_app::TestAppRegistrar> registrar_;
TestBadgeManagerDelegate* delegate_;
BadgeManager* badge_manager_;
std::unique_ptr<TestingProfile> profile_;
content::BrowserTaskEnvironment task_environment_;
DISALLOW_COPY_AND_ASSIGN(BadgeManagerDelegateUnittest);
};
TEST_F(BadgeManagerDelegateUnittest, InScopeAppsAreBadged) {
const web_app::AppId& kApp1 = "app1";
const web_app::AppId& kApp2 = "app2";
registrar()->AddExternalApp(kApp1, {GURL("https://example.com/app1")});
registrar()->AddExternalApp(kApp2, {GURL("https://example.com/app2")});
badge_manager()->SetBadgeForTesting(GURL("https://example.com/"),
base::nullopt);
EXPECT_EQ(2UL, delegate()->set_app_badges().size());
EXPECT_EQ(kApp1, delegate()->set_app_badges()[0].first);
EXPECT_EQ(base::nullopt, delegate()->set_app_badges()[0].second);
EXPECT_EQ(kApp2, delegate()->set_app_badges()[1].first);
EXPECT_EQ(base::nullopt, delegate()->set_app_badges()[1].second);
}
TEST_F(BadgeManagerDelegateUnittest, InScopeAppsAreCleared) {
const web_app::AppId& kApp1 = "app1";
const web_app::AppId& kApp2 = "app2";
registrar()->AddExternalApp(kApp1, {GURL("https://example.com/app1")});
registrar()->AddExternalApp(kApp2, {GURL("https://example.com/app2")});
badge_manager()->SetBadgeForTesting(GURL("https://example.com/"),
base::nullopt);
badge_manager()->ClearBadgeForTesting(GURL("https://example.com/"));
EXPECT_EQ(2UL, delegate()->cleared_app_badges().size());
EXPECT_EQ(kApp1, delegate()->cleared_app_badges()[0]);
EXPECT_EQ(kApp2, delegate()->cleared_app_badges()[1]);
}
} // namespace badging
...@@ -12,30 +12,37 @@ ...@@ -12,30 +12,37 @@
#include "chrome/browser/ui/browser_list.h" #include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h" #include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
#include "ui/base/l10n/l10n_util.h" #include "ui/base/l10n/l10n_util.h"
#include "ui/strings/grit/ui_strings.h" #include "ui/strings/grit/ui_strings.h"
namespace badging { namespace badging {
BadgeManagerDelegateWin::BadgeManagerDelegateWin(Profile* profile) namespace {
: BadgeManagerDelegate(profile) {}
void BadgeManagerDelegateWin::OnBadgeSet(const std::string& app_id, // Determines the badge contents and alt text.
base::Optional<uint64_t> contents) { // base::nullopt if the badge is not set.
auto badge_string = badging::GetBadgeString(contents); // otherwise a pair (badge_content, badge_alt_text), based on the content of the
// There are 3 different cases: // badge.
base::Optional<std::pair<std::string, std::string>> GetBadgeContentAndAlt(
const base::Optional<BadgeManager::BadgeValue>& badge) {
// If there is no badge, there is no contents or alt text.
if (!badge)
return base::nullopt;
std::string badge_string = badging::GetBadgeString(badge.value());
// There are 3 different cases when the badge has a value:
// 1. |contents| is between 1 and 99 inclusive => Set the accessibility text // 1. |contents| is between 1 and 99 inclusive => Set the accessibility text
// to a pluralized notification count (e.g. 4 Unread Notifications). // to a pluralized notification count (e.g. 4 Unread Notifications).
// 2. |contents| is greater than 99 => Set the accessibility text to // 2. |contents| is greater than 99 => Set the accessibility text to
// More than |kMaxBadgeContent| unread notifications, so the // More than |kMaxBadgeContent| unread notifications, so the
// accessibility text matches what is displayed on the badge (e.g. More // accessibility text matches what is displayed on the badge (e.g. More
// than 99 notifications). // than 99 notifications).
// 3. |contents| doesn't have a value (i.e. the badge is set to 'flag') => Set // 3. The badge is set to 'flag' => Set the accessibility text to something
// the accessibility text to something less specific (e.g. Unread // less specific (e.g. Unread Notifications).
// Notifications).
std::string badge_alt_string; std::string badge_alt_string;
if (contents) { if (badge.value()) {
uint64_t value = contents.value(); uint64_t value = badge.value().value();
badge_alt_string = value <= badging::kMaxBadgeContent badge_alt_string = value <= badging::kMaxBadgeContent
// Case 1. // Case 1.
? l10n_util::GetPluralStringFUTF8( ? l10n_util::GetPluralStringFUTF8(
...@@ -50,25 +57,31 @@ void BadgeManagerDelegateWin::OnBadgeSet(const std::string& app_id, ...@@ -50,25 +57,31 @@ void BadgeManagerDelegateWin::OnBadgeSet(const std::string& app_id,
l10n_util::GetStringUTF8(IDS_BADGE_UNREAD_NOTIFICATIONS_UNSPECIFIED); l10n_util::GetStringUTF8(IDS_BADGE_UNREAD_NOTIFICATIONS_UNSPECIFIED);
} }
for (Browser* browser : *BrowserList::GetInstance()) { return std::make_pair(badge_string, badge_alt_string);
if (!IsAppBrowser(browser, app_id))
continue;
auto* window = browser->window()->GetNativeWindow();
taskbar::DrawTaskbarDecorationString(window, badge_string,
badge_alt_string);
}
} }
void BadgeManagerDelegateWin::OnBadgeCleared(const std::string& app_id) { } // namespace
BadgeManagerDelegateWin::BadgeManagerDelegateWin(
Profile* profile,
BadgeManager* badge_manager,
web_app::AppRegistrar* registrar)
: BadgeManagerDelegate(profile, badge_manager, registrar) {}
void BadgeManagerDelegateWin::OnAppBadgeUpdated(const web_app::AppId& app_id) {
const auto& content_and_alt = GetBadgeContentAndAlt(GetAppBadgeValue(app_id));
for (Browser* browser : *BrowserList::GetInstance()) { for (Browser* browser : *BrowserList::GetInstance()) {
if (!IsAppBrowser(browser, app_id)) if (!IsAppBrowser(browser, app_id))
continue; continue;
// Restore the decoration to whatever it is naturally (either nothing or a auto* window = browser->window()->GetNativeWindow();
// profile picture badge). if (content_and_alt) {
taskbar::UpdateTaskbarDecoration(browser->profile(), taskbar::DrawTaskbarDecorationString(window, content_and_alt->first,
browser->window()->GetNativeWindow()); content_and_alt->second);
} else {
taskbar::UpdateTaskbarDecoration(browser->profile(), window);
}
} }
} }
...@@ -76,7 +89,7 @@ bool BadgeManagerDelegateWin::IsAppBrowser(Browser* browser, ...@@ -76,7 +89,7 @@ bool BadgeManagerDelegateWin::IsAppBrowser(Browser* browser,
const std::string& app_id) { const std::string& app_id) {
return browser->app_controller() && return browser->app_controller() &&
browser->app_controller()->GetAppId() == app_id && browser->app_controller()->GetAppId() == app_id &&
browser->profile() == profile_; browser->profile() == profile();
} }
} // namespace badging } // namespace badging
...@@ -13,17 +13,22 @@ ...@@ -13,17 +13,22 @@
class Profile; class Profile;
namespace web_app {
class AppRegistrar;
}
namespace badging { namespace badging {
class BadgeManager;
// Windows specific implementation of the BadgeManagerDelegate. // Windows specific implementation of the BadgeManagerDelegate.
class BadgeManagerDelegateWin : public BadgeManagerDelegate { class BadgeManagerDelegateWin : public BadgeManagerDelegate {
public: public:
explicit BadgeManagerDelegateWin(Profile* profile); explicit BadgeManagerDelegateWin(Profile* profile,
BadgeManager* badge_manager,
void OnBadgeSet(const std::string& app_id, web_app::AppRegistrar* registrar);
base::Optional<uint64_t> contents) override;
void OnBadgeCleared(const std::string& app_id) override; void OnAppBadgeUpdated(const web_app::AppId& app_id) override;
private: private:
// Determines if a browser is for a specific hosted app, on this profile. // Determines if a browser is for a specific hosted app, on this profile.
......
// Copyright 2019 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/badging/test_badge_manager_delegate.h"
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/badging/badge_manager_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
namespace badging {
TestBadgeManagerDelegate::TestBadgeManagerDelegate(
Profile* profile,
BadgeManager* badge_manager,
web_app::AppRegistrar* registrar)
: BadgeManagerDelegate(profile, badge_manager, registrar) {}
TestBadgeManagerDelegate::~TestBadgeManagerDelegate() = default;
void TestBadgeManagerDelegate::SetOnBadgeChanged(
base::RepeatingCallback<void()> on_badge_changed) {
on_badge_changed_ = on_badge_changed;
}
void TestBadgeManagerDelegate::OnBadgeUpdated(const GURL& scope) {
BadgeManagerDelegate::OnBadgeUpdated(scope);
const auto& value = badge_manager()->GetBadgeValue(scope);
if (!value)
cleared_scope_badges_.push_back(scope);
else
set_scope_badges_.push_back(std::make_pair(scope, value.value()));
if (on_badge_changed_)
on_badge_changed_.Run();
}
void TestBadgeManagerDelegate::OnAppBadgeUpdated(const web_app::AppId& app_id) {
const auto& value = GetAppBadgeValue(app_id);
if (!value)
cleared_app_badges_.push_back(app_id);
else
set_app_badges_.push_back(std::make_pair(app_id, value.value()));
}
void TestBadgeManagerDelegate::ResetBadges() {
cleared_app_badges_.clear();
set_app_badges_.clear();
cleared_scope_badges_.clear();
set_scope_badges_.clear();
}
} // namespace badging
// Copyright 2019 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_BADGING_TEST_BADGE_MANAGER_DELEGATE_H_
#define CHROME_BROWSER_BADGING_TEST_BADGE_MANAGER_DELEGATE_H_
#include <vector>
#include "base/callback.h"
#include "base/optional.h"
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/badging/badge_manager_delegate.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
class Profile;
namespace web_app {
class AppRegistrar;
}
namespace badging {
class BadgeManager;
using AppBadge = std::pair<web_app::AppId, BadgeManager::BadgeValue>;
using ScopeBadge = std::pair<GURL, BadgeManager::BadgeValue>;
// Testing delegate that records badge changes for apps.
class TestBadgeManagerDelegate : public BadgeManagerDelegate {
public:
TestBadgeManagerDelegate(Profile* profile,
BadgeManager* badge_manager,
web_app::AppRegistrar* registrar);
~TestBadgeManagerDelegate() override;
// Sets a callback to fire when a badge is set or cleared.
void SetOnBadgeChanged(base::RepeatingCallback<void()> on_badge_changed);
// Resets the lists of cleared and set badges.
void ResetBadges();
std::vector<web_app::AppId> cleared_app_badges() {
return cleared_app_badges_;
}
std::vector<AppBadge> set_app_badges() { return set_app_badges_; }
std::vector<GURL> cleared_scope_badges() { return cleared_scope_badges_; }
std::vector<ScopeBadge> set_scope_badges() { return set_scope_badges_; }
// BadgeManagerDelegate:
void OnBadgeUpdated(const GURL& scope) override;
protected:
// BadgeManagerDelegate:
void OnAppBadgeUpdated(const web_app::AppId& app_badge) override;
private:
base::RepeatingCallback<void()> on_badge_changed_;
std::vector<web_app::AppId> cleared_app_badges_;
std::vector<AppBadge> set_app_badges_;
std::vector<GURL> cleared_scope_badges_;
std::vector<ScopeBadge> set_scope_badges_;
};
} // namespace badging
#endif // CHROME_BROWSER_BADGING_TEST_BADGE_MANAGER_DELEGATE_H_
...@@ -3,10 +3,11 @@ ...@@ -3,10 +3,11 @@
// found in the LICENSE file. // found in the LICENSE file.
#include "chrome/browser/badging/badge_manager.h" #include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/badging/badge_manager_delegate.h"
#include "chrome/browser/badging/badge_manager_factory.h" #include "chrome/browser/badging/badge_manager_factory.h"
#include "chrome/browser/badging/test_badge_manager_delegate.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/web_applications/web_app_controller_browsertest.h" #include "chrome/browser/ui/web_applications/web_app_controller_browsertest.h"
#include "chrome/browser/web_applications/components/web_app_provider_base.h"
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
using content::RenderFrameHost; using content::RenderFrameHost;
...@@ -16,40 +17,9 @@ namespace web_app { ...@@ -16,40 +17,9 @@ namespace web_app {
class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest { class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest {
public: public:
// Listens to BadgeManager events and forwards them to the test class. WebAppBadgingBrowserTest()
class TestBadgeManagerDelegate : public badging::BadgeManagerDelegate { : WebAppControllerBrowserTest(),
public: cross_origin_https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
using SetBadgeCallback =
base::RepeatingCallback<void(const AppId&, base::Optional<uint64_t>)>;
using ClearBadgeCallback = base::RepeatingCallback<void(const AppId&)>;
using ChangeFailedCallback = base::RepeatingCallback<void()>;
TestBadgeManagerDelegate(Profile* profile,
SetBadgeCallback on_set_badge,
ClearBadgeCallback on_clear_badge,
ChangeFailedCallback on_change_failed)
: badging::BadgeManagerDelegate(profile),
on_set_badge_(on_set_badge),
on_clear_badge_(on_clear_badge),
on_change_failed_(on_change_failed) {}
void OnBadgeSet(const AppId& app_id,
base::Optional<uint64_t> contents) override {
on_set_badge_.Run(app_id, contents);
}
void OnBadgeCleared(const AppId& app_id) override {
on_clear_badge_.Run(app_id);
}
void OnBadgeChangeIgnoredForTesting() override { on_change_failed_.Run(); }
private:
SetBadgeCallback on_set_badge_;
ClearBadgeCallback on_clear_badge_;
ChangeFailedCallback on_change_failed_;
};
void SetUpCommandLine(base::CommandLine* command_line) override { void SetUpCommandLine(base::CommandLine* command_line) override {
WebAppControllerBrowserTest::SetUpCommandLine(command_line); WebAppControllerBrowserTest::SetUpCommandLine(command_line);
...@@ -59,25 +29,33 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest { ...@@ -59,25 +29,33 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest {
void SetUpOnMainThread() override { void SetUpOnMainThread() override {
WebAppControllerBrowserTest::SetUpOnMainThread(); WebAppControllerBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(cross_origin_https_server_.Start());
ASSERT_TRUE(https_server()->Start()); ASSERT_TRUE(https_server()->Start());
ASSERT_TRUE(embedded_test_server()->Start()); ASSERT_TRUE(embedded_test_server()->Start());
AppId app_id = InstallPWA(https_server()->GetURL( GURL cross_site_frame_url =
"/ssl/page_with_in_scope_and_cross_site_frame.html")); cross_origin_https_server_.GetURL("/ssl/google.html");
// Note: The url for the cross site frame is embedded in the query string.
GURL app_url = https_server()->GetURL(
"/ssl/page_with_in_scope_and_cross_site_frame.html?url=" +
cross_site_frame_url.spec());
AppId app_id = InstallPWA(app_url);
content::WebContents* web_contents = OpenApplication(app_id); content::WebContents* web_contents = OpenApplication(app_id);
// There should be exactly 3 frames: // There should be exactly 3 frames:
// 1) The main frame. // 1) The main frame.
// 2) A cross site frame, on https://example.com/ // 2) A cross site frame, on |cross_site_frame_url|.
// 3) A sub frame in the app's scope. // 3) A sub frame in the app's scope.
auto frames = web_contents->GetAllFrames(); auto frames = web_contents->GetAllFrames();
ASSERT_EQ(3u, frames.size()); ASSERT_EQ(3u, frames.size());
main_frame_ = web_contents->GetMainFrame(); main_frame_ = web_contents->GetMainFrame();
for (auto* frame : frames) { for (auto* frame : frames) {
if (frame->GetLastCommittedURL() == "https://example.com/") if (url::IsSameOriginWith(frame->GetLastCommittedURL(),
cross_site_frame_ = frame; main_frame_->GetLastCommittedURL())) {
else if (frame != main_frame_)
in_scope_frame_ = frame; in_scope_frame_ = frame;
} else if (frame != main_frame_) {
cross_site_frame_ = frame;
}
} }
ASSERT_TRUE(main_frame_); ASSERT_TRUE(main_frame_);
...@@ -86,36 +64,38 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest { ...@@ -86,36 +64,38 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest {
awaiter_ = std::make_unique<base::RunLoop>(); awaiter_ = std::make_unique<base::RunLoop>();
std::unique_ptr<badging::BadgeManagerDelegate> delegate = badging::BadgeManager* badge_manager =
std::make_unique<TestBadgeManagerDelegate>( badging::BadgeManagerFactory::GetInstance()->GetForProfile(profile());
profile(),
base::BindRepeating(&WebAppBadgingBrowserTest::OnBadgeSet,
base::Unretained(this)),
base::BindRepeating(&WebAppBadgingBrowserTest::OnBadgeCleared,
base::Unretained(this)),
base::BindRepeating(&WebAppBadgingBrowserTest::OnBadgeChangeFailed,
base::Unretained(this)));
badging::BadgeManagerFactory::GetInstance()
->GetForProfile(profile())
->SetDelegate(std::move(delegate));
}
void OnBadgeSet(const AppId& app_id, base::Optional<uint64_t> badge_content) { // The delegate is owned by the badge manager. We hold a pointer to it for
if (badge_content.has_value()) // the test.
last_badge_content_ = badge_content; std::unique_ptr<badging::TestBadgeManagerDelegate> owned_delegate =
else std::make_unique<badging::TestBadgeManagerDelegate>(
was_flagged_ = true; profile(), badge_manager,
&WebAppProviderBase::GetProviderBase(profile())->registrar());
owned_delegate->SetOnBadgeChanged(base::BindRepeating(
&WebAppBadgingBrowserTest::OnBadgeChanged, base::Unretained(this)));
delegate_ = owned_delegate.get();
awaiter_->Quit(); badge_manager->SetDelegate(std::move(owned_delegate));
} }
void OnBadgeCleared(const AppId& app_id) { void OnBadgeChanged() {
was_cleared_ = true; // This is only set up to deal with one badge change at a time, in order to
awaiter_->Quit(); // make asserting the result of a badge change easier.
} int total_changes = delegate_->cleared_app_badges().size() +
delegate_->set_app_badges().size();
EXPECT_LE(total_changes, 1);
// A change is a failure if it doesn't set or clear any badges.
change_failed_ = total_changes == 0;
was_cleared_ = delegate_->cleared_app_badges().size();
if (delegate_->set_app_badges().size() == 1) {
last_badge_content_ = delegate_->set_app_badges()[0].second;
was_flagged_ = last_badge_content_ == base::nullopt;
}
void OnBadgeChangeFailed() {
change_failed_ = true;
awaiter_->Quit(); awaiter_->Quit();
} }
...@@ -127,6 +107,7 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest { ...@@ -127,6 +107,7 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest {
change_failed_ = false; change_failed_ = false;
last_badge_content_ = base::nullopt; last_badge_content_ = base::nullopt;
awaiter_ = std::make_unique<base::RunLoop>(); awaiter_ = std::make_unique<base::RunLoop>();
delegate_->ResetBadges();
ASSERT_TRUE(content::ExecuteScript(on, script)); ASSERT_TRUE(content::ExecuteScript(on, script));
...@@ -148,8 +129,29 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest { ...@@ -148,8 +129,29 @@ class WebAppBadgingBrowserTest : public WebAppControllerBrowserTest {
private: private:
std::unique_ptr<base::RunLoop> awaiter_; std::unique_ptr<base::RunLoop> awaiter_;
badging::TestBadgeManagerDelegate* delegate_;
net::EmbeddedTestServer cross_origin_https_server_;
}; };
// Tests that the badge for the main frame is not affected by changing the badge
// of a cross site subframe.
IN_PROC_BROWSER_TEST_P(WebAppBadgingBrowserTest,
BadgeCannotBeChangedFromCrossSiteFrame) {
// Clearing from cross site frame should be a no-op.
ExecuteScriptAndWaitForBadgeChange("ExperimentalBadge.clear()",
cross_site_frame_);
ASSERT_FALSE(was_cleared_);
ASSERT_FALSE(was_flagged_);
ASSERT_TRUE(change_failed_);
// Setting from cross site frame should be a no-op.
ExecuteScriptAndWaitForBadgeChange("ExperimentalBadge.set(77)",
cross_site_frame_);
ASSERT_FALSE(was_cleared_);
ASSERT_FALSE(was_flagged_);
ASSERT_TRUE(change_failed_);
}
// Tests that setting the badge to an integer will be propagated across // Tests that setting the badge to an integer will be propagated across
// processes. // processes.
IN_PROC_BROWSER_TEST_P(WebAppBadgingBrowserTest, BadgeCanBeSetToAnInteger) { IN_PROC_BROWSER_TEST_P(WebAppBadgingBrowserTest, BadgeCanBeSetToAnInteger) {
...@@ -213,24 +215,6 @@ IN_PROC_BROWSER_TEST_P(WebAppBadgingBrowserTest, ...@@ -213,24 +215,6 @@ IN_PROC_BROWSER_TEST_P(WebAppBadgingBrowserTest,
ASSERT_EQ(base::nullopt, last_badge_content_); ASSERT_EQ(base::nullopt, last_badge_content_);
} }
// Tests that the badge cannot be set and cleared from a cross site frame.
IN_PROC_BROWSER_TEST_P(WebAppBadgingBrowserTest,
BadgeCannotBeChangedFromCrossSiteFrame) {
// Clearing from cross site frame should be a no-op.
ExecuteScriptAndWaitForBadgeChange("ExperimentalBadge.clear()",
cross_site_frame_);
ASSERT_FALSE(was_cleared_);
ASSERT_FALSE(was_flagged_);
ASSERT_TRUE(change_failed_);
// Setting from cross site frame should be a no-op.
ExecuteScriptAndWaitForBadgeChange("ExperimentalBadge.set(77)",
cross_site_frame_);
ASSERT_FALSE(was_cleared_);
ASSERT_FALSE(was_flagged_);
ASSERT_TRUE(change_failed_);
}
INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P(
/* no prefix */, /* no prefix */,
WebAppBadgingBrowserTest, WebAppBadgingBrowserTest,
......
...@@ -798,6 +798,8 @@ if (!is_android) { ...@@ -798,6 +798,8 @@ if (!is_android) {
"../browser/background_fetch/background_fetch_browsertest.cc", "../browser/background_fetch/background_fetch_browsertest.cc",
"../browser/background_sync/background_sync_browsertest.cc", "../browser/background_sync/background_sync_browsertest.cc",
"../browser/background_sync/background_sync_metrics_browsertest.cc", "../browser/background_sync/background_sync_metrics_browsertest.cc",
"../browser/badging/test_badge_manager_delegate.cc",
"../browser/badging/test_badge_manager_delegate.h",
"../browser/banners/app_banner_manager_browsertest.cc", "../browser/banners/app_banner_manager_browsertest.cc",
"../browser/banners/app_banner_manager_browsertest_base.cc", "../browser/banners/app_banner_manager_browsertest_base.cc",
"../browser/banners/app_banner_manager_browsertest_base.h", "../browser/banners/app_banner_manager_browsertest_base.h",
...@@ -3594,7 +3596,10 @@ test("unit_tests") { ...@@ -3594,7 +3596,10 @@ test("unit_tests") {
# !is_android # !is_android
sources += [ sources += [
# Badging isn't supported on Android. # Badging isn't supported on Android.
"../browser/badging/badge_manager_delegate_unittest.cc",
"../browser/badging/badge_manager_unittest.cc", "../browser/badging/badge_manager_unittest.cc",
"../browser/badging/test_badge_manager_delegate.cc",
"../browser/badging/test_badge_manager_delegate.h",
# Bookmark export/import are handled via the BookmarkColumns # Bookmark export/import are handled via the BookmarkColumns
# ContentProvider. # ContentProvider.
......
...@@ -3,6 +3,11 @@ ...@@ -3,6 +3,11 @@
</head> </head>
<body> <body>
<iframe name="in-scope" src="/ssl/google.html" id="in-scope"></iframe> <iframe name="in-scope" src="/ssl/google.html" id="in-scope"></iframe>
<iframe name="cross-site" src="https://example.com" id="cross-site"></iframe> <iframe name="cross-site" id="cross-site"></iframe>
<script type="text/javascript">
const crossFrame = document.getElementById('cross-site');
const crossSiteUrl = new URLSearchParams(location.search).get('url');
crossFrame.src = crossSiteUrl;
</script>
</body> </body>
</html> </html>
...@@ -4,17 +4,28 @@ ...@@ -4,17 +4,28 @@
module blink.mojom; module blink.mojom;
import "url/mojom/url.mojom";
// A value that a badge can hold.
union BadgeValue {
// Represents the special value |flag|. This indicates that a badge should be
// set but that no data should be displayed in it.
// Note: The value of this field is unused (see https://crbug.com/999900).
uint8 flag;
// Represents a numerical badge. Note: This must not be zero (setting the
// badge to 0 should be translated into a clear).
uint64 number;
};
// Interface for handling badge messages from frames and subframes. // Interface for handling badge messages from frames and subframes.
interface BadgeService { interface BadgeService {
// Sets the badge for the PWA corresponding to this request to be a // Asks the browser process to set a badge.
// non-zero, positive integer. // |scope| specifies which badges to set. Note: This must be on the same
SetInteger(uint64 content); // origin as the caller.
SetBadge(url.mojom.Url scope, BadgeValue value);
// Sets the badge for the PWA corresponding to this request to be a
// flag marker.
SetFlag();
// Clears the badge (if it exists) for the PWA corresponding to // Asks the browser to clear a badge.
// this request. // |scope| specifies the badges to clear. Note: This must be on the same
ClearBadge(); // origin as the caller.
ClearBadge(url.mojom.Url scope);
}; };
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
#include "services/service_manager/public/cpp/interface_provider.h" #include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" #include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/modules/badging/badge_options.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h" #include "third_party/blink/renderer/platform/bindings/script_state.h"
...@@ -28,42 +29,62 @@ Badge* Badge::From(ExecutionContext* context) { ...@@ -28,42 +29,62 @@ Badge* Badge::From(ExecutionContext* context) {
// static // static
void Badge::set(ScriptState* script_state, ExceptionState& exception_state) { void Badge::set(ScriptState* script_state, ExceptionState& exception_state) {
BadgeFromState(script_state)->SetFlag(); Badge::set(script_state, BadgeOptions::Create(), exception_state);
}
// static
void Badge::set(ScriptState* script_state,
const BadgeOptions* options,
ExceptionState& exception_state) {
BadgeFromState(script_state)
->SetBadge(options->scope(), mojom::blink::BadgeValue::NewFlag(0));
} }
// static // static
void Badge::set(ScriptState* script_state, void Badge::set(ScriptState* script_state,
uint64_t content, uint64_t content,
ExceptionState& exception_state) { ExceptionState& exception_state) {
if (content == 0) Badge::set(script_state, content, BadgeOptions::Create(), exception_state);
BadgeFromState(script_state)->Clear();
else
BadgeFromState(script_state)->SetInteger(content);
} }
// static // static
void Badge::clear(ScriptState* script_state) { void Badge::set(ScriptState* script_state,
BadgeFromState(script_state)->Clear(); uint64_t content,
const BadgeOptions* options,
ExceptionState& exception_state) {
if (content == 0) {
Badge::clear(script_state, options);
} else {
BadgeFromState(script_state)
->SetBadge(options->scope(),
mojom::blink::BadgeValue::NewNumber(content));
}
} }
void Badge::SetInteger(uint64_t content) { // static
badge_service_->SetInteger(content); void Badge::clear(ScriptState* script_state, const BadgeOptions* options) {
BadgeFromState(script_state)->ClearBadge(options->scope());
} }
void Badge::SetFlag() { void Badge::SetBadge(WTF::String scope, mojom::blink::BadgeValuePtr value) {
badge_service_->SetFlag(); // Resolve |scope| against the URL of the current document/worker.
KURL scope_url = KURL(execution_context_->Url(), scope);
badge_service_->SetBadge(scope_url, std::move(value));
} }
void Badge::Clear() { void Badge::ClearBadge(WTF::String scope) {
badge_service_->ClearBadge(); // Resolve |scope| against the URL of the current document/worker.
badge_service_->ClearBadge(KURL(execution_context_->Url(), scope));
} }
void Badge::Trace(blink::Visitor* visitor) { void Badge::Trace(blink::Visitor* visitor) {
Supplement<ExecutionContext>::Trace(visitor); Supplement<ExecutionContext>::Trace(visitor);
ScriptWrappable::Trace(visitor); ScriptWrappable::Trace(visitor);
visitor->Trace(execution_context_);
} }
Badge::Badge(ExecutionContext* context) { Badge::Badge(ExecutionContext* context) : execution_context_(context) {
context->GetInterfaceProvider()->GetInterface( context->GetInterfaceProvider()->GetInterface(
badge_service_.BindNewPipeAndPassReceiver()); badge_service_.BindNewPipeAndPassReceiver());
DCHECK(badge_service_); DCHECK(badge_service_);
......
...@@ -9,9 +9,11 @@ ...@@ -9,9 +9,11 @@
#include "third_party/blink/public/mojom/badging/badging.mojom-blink.h" #include "third_party/blink/public/mojom/badging/badging.mojom-blink.h"
#include "third_party/blink/renderer/platform/bindings/script_wrappable.h" #include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
#include "third_party/blink/renderer/platform/supplementable.h" #include "third_party/blink/renderer/platform/supplementable.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace blink { namespace blink {
class BadgeOptions;
class ExceptionState; class ExceptionState;
class ExecutionContext; class ExecutionContext;
class ScriptState; class ScriptState;
...@@ -30,13 +32,17 @@ class Badge final : public ScriptWrappable, ...@@ -30,13 +32,17 @@ class Badge final : public ScriptWrappable,
~Badge() override; ~Badge() override;
// Badge IDL interface. // Badge IDL interface.
static void set(ScriptState*, const BadgeOptions*, ExceptionState&);
static void set(ScriptState*,
uint64_t content,
const BadgeOptions*,
ExceptionState&);
static void set(ScriptState*, ExceptionState&); static void set(ScriptState*, ExceptionState&);
static void set(ScriptState*, uint64_t content, ExceptionState&); static void set(ScriptState*, uint64_t content, ExceptionState&);
static void clear(ScriptState*); static void clear(ScriptState*, const BadgeOptions*);
void SetInteger(uint64_t content); void SetBadge(WTF::String scope, mojom::blink::BadgeValuePtr value);
void SetFlag(); void ClearBadge(WTF::String scope);
void Clear();
void Trace(blink::Visitor*) override; void Trace(blink::Visitor*) override;
...@@ -44,6 +50,7 @@ class Badge final : public ScriptWrappable, ...@@ -44,6 +50,7 @@ class Badge final : public ScriptWrappable,
static Badge* BadgeFromState(ScriptState* script_state); static Badge* BadgeFromState(ScriptState* script_state);
mojo::Remote<blink::mojom::blink::BadgeService> badge_service_; mojo::Remote<blink::mojom::blink::BadgeService> badge_service_;
Member<ExecutionContext> execution_context_;
}; };
} // namespace blink } // namespace blink
......
// Copyright 2019 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.
// The options used to decide how to apply a badge.
// See the explainer https://github.com/WICG/badging/blob/master/explainer.md
dictionary BadgeOptions {
// The scope of the badge. This is resolved against the URL of the current
// page.
// https://github.com/WICG/badging/blob/master/explainer.md#badge-scope
DOMString scope = "/";
};
\ No newline at end of file
...@@ -12,6 +12,6 @@ ...@@ -12,6 +12,6 @@
ImplementedAs=Badge ImplementedAs=Badge
] interface ExperimentalBadge { ] interface ExperimentalBadge {
[CallWith=ScriptState, RaisesException, MeasureAs=BadgeSet] [CallWith=ScriptState, RaisesException, MeasureAs=BadgeSet]
static void set(optional [EnforceRange] unsigned long long contents); static void set(optional [EnforceRange] unsigned long long contents, optional BadgeOptions options);
[CallWith=ScriptState, MeasureAs=BadgeClear] static void clear(); [CallWith=ScriptState, MeasureAs=BadgeClear] static void clear(optional BadgeOptions options);
}; };
...@@ -572,6 +572,7 @@ modules_dictionary_idl_files = ...@@ -572,6 +572,7 @@ modules_dictionary_idl_files =
"background_sync/background_sync_options.idl", "background_sync/background_sync_options.idl",
"background_sync/sync_event_init.idl", "background_sync/sync_event_init.idl",
"background_sync/periodic_sync_event_init.idl", "background_sync/periodic_sync_event_init.idl",
"badging/badge_options.idl",
"bluetooth/bluetooth_advertising_event_init.idl", "bluetooth/bluetooth_advertising_event_init.idl",
"bluetooth/bluetooth_le_scan_filter_init.idl", "bluetooth/bluetooth_le_scan_filter_init.idl",
"bluetooth/bluetooth_le_scan_options.idl", "bluetooth/bluetooth_le_scan_options.idl",
......
...@@ -11,9 +11,9 @@ ...@@ -11,9 +11,9 @@
<body> <body>
<script> <script>
badge_test(() => { ExperimentalBadge.set(-1); }, 'setInteger', 'TypeError'); badge_test(() => { ExperimentalBadge.set(-1); }, undefined, 'TypeError');
badge_test(() => { ExperimentalBadge.set("Foo"); }, 'setInteger', 'TypeError'); badge_test(() => { ExperimentalBadge.set("Foo"); }, undefined, 'TypeError');
</script> </script>
</body> </body>
......
...@@ -11,16 +11,16 @@ ...@@ -11,16 +11,16 @@
<body> <body>
<script> <script>
badge_test(() => { ExperimentalBadge.set(); }, 'setFlag'); badge_test(() => { ExperimentalBadge.set(); }, 'flag');
badge_test(() => { ExperimentalBadge.set(undefined); }, 'setFlag'); badge_test(() => { ExperimentalBadge.set(undefined); }, 'flag');
badge_test(() => { ExperimentalBadge.set(1); }, 'setInteger'); badge_test(() => { ExperimentalBadge.set(1); }, 'number');
// Setting the Badge to 0 should be equivalent to clearing the badge. // Setting the Badge to 0 should be equivalent to clearing the badge.
badge_test(() => { ExperimentalBadge.set(0); }, 'clearBadge'); badge_test(() => { ExperimentalBadge.set(0); }, 'clear');
badge_test(() => { ExperimentalBadge.clear(); }, 'clearBadge'); badge_test(() => { ExperimentalBadge.clear(); }, 'clear');
</script> </script>
</body> </body>
......
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
<script type="text/plain" id="tested"> <script type="text/plain" id="tested">
interface ExperimentalBadge { interface ExperimentalBadge {
[CallWith=ScriptState, RaisesException] [CallWith=ScriptState, RaisesException]
static void set(optional [EnforceRange] unsigned long long contents); static void set(optional [EnforceRange] unsigned long long contents, optional BadgeOptions options);
[CallWith=ScriptState] static void clear(); [CallWith=ScriptState] static void clear(optional BadgeOptions options);
}; };
</script> </script>
<script> <script>
......
...@@ -10,35 +10,36 @@ class MockBadgeService { ...@@ -10,35 +10,36 @@ class MockBadgeService {
this.interceptor_.start(); this.interceptor_.start();
} }
init_(expectCalled) { init_(expectedAction) {
this.expectCalled_ = expectCalled; this.expectedAction = expectedAction;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.reject_ = reject; this.reject_ = reject;
this.resolve_ = resolve; this.resolve_ = resolve;
}); });
} }
setInteger(contents) { setBadge(scope, value) {
// Accessing number when the union is a flag will throw, so read the
// value in a try catch.
let number;
try { try {
assert_equals(this.expectCalled_, 'setInteger'); number = value.number;
this.resolve_();
} catch (error) { } catch (error) {
this.reject_(error); number = undefined;
} }
}
setFlag() {
try { try {
assert_equals(this.expectCalled_, 'setFlag'); const action = number === undefined ? 'flag' : 'number';
assert_equals(this.expectedAction, action);
this.resolve_(); this.resolve_();
} catch (error) { } catch (error) {
this.reject(error); this.reject_();
} }
} }
clearBadge() { clearBadge(scope) {
try { try {
assert_equals(this.expectCalled_, 'clearBadge'); assert_equals(this.expectedAction, 'clear');
this.resolve_(); this.resolve_();
} catch (error) { } catch (error) {
this.reject_(error); this.reject_(error);
...@@ -63,9 +64,9 @@ function callAndObserveErrors(func, expectedErrorName) { ...@@ -63,9 +64,9 @@ function callAndObserveErrors(func, expectedErrorName) {
}); });
} }
function badge_test(func, expectCalled, expectError) { function badge_test(func, expectedAction, expectError) {
promise_test(() => { promise_test(() => {
let mockPromise = mockBadgeService.init_(expectCalled); let mockPromise = mockBadgeService.init_(expectedAction);
return Promise.race([callAndObserveErrors(func, expectError), mockPromise]); return Promise.race([callAndObserveErrors(func, expectError), mockPromise]);
}); });
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment