Commit 8b5b00d6 authored by johnme's avatar johnme Committed by Commit bot

Push API: Grace - allow one in ten pushes to show no notification.

For developers who have opted in to showing a user-visible UI change on
every push (in exchange for showing the user a less scary permission
prompt), we currently[1] enforce that each push shows a notification.

This patch allows one in ten pushes to show no notification, so
occasional bugs in developer push handlers don't needlessly spam the
user.

Specifically, it keeps track of whether the last (up to) 10 push
messages showed a notification (ignoring pushes didn't show a
notification but were exempt, e.g. due to https://crrev.com/866443003)
and only shows a forced notification if one of those (up to) 10 also
failed to show a notification.

[1]: (since https://crrev.com/842233003)

BUG=437277

Review URL: https://codereview.chromium.org/883743002

Cr-Commit-Position: refs/heads/master@{#314851}
parent fa5c5a84
...@@ -499,39 +499,34 @@ IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, ...@@ -499,39 +499,34 @@ IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
browser()->tab_strip_model()->GetActiveWebContents(); browser()->tab_strip_model()->GetActiveWebContents();
// If the site is visible in an active tab, we should not force a notification // If the site is visible in an active tab, we should not force a notification
// to be shown. // to be shown. Try it twice, since we allow one mistake per 10 push events.
GCMClient::IncomingMessage message; GCMClient::IncomingMessage message;
message.data["data"] = "testdata"; for (int n = 0; n < 2; n++) {
push_service()->OnMessage(app_id.ToString(), message); message.data["data"] = "testdata";
ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result)); push_service()->OnMessage(app_id.ToString(), message);
EXPECT_EQ("testdata", script_result); ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result));
ASSERT_EQ(0u, notification_manager()->GetNotificationCount()); EXPECT_EQ("testdata", script_result);
EXPECT_EQ(0u, notification_manager()->GetNotificationCount());
}
// Open a blank foreground tab so site is no longer visible. // Open a blank foreground tab so site is no longer visible.
ui_test_utils::NavigateToURLWithDisposition( ui_test_utils::NavigateToURLWithDisposition(
browser(), GURL("about:blank"), NEW_FOREGROUND_TAB, browser(), GURL("about:blank"), NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB); ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB);
// If the Service Worker push event handler shows a notification, we should // If the Service Worker push event handler does not show a notification, we
// not show a forced one. // should show a forced one, but only on the 2nd occurrence since we allow one
message.data["data"] = "shownotification"; // mistake per 10 push events.
message.data["data"] = "testdata";
push_service()->OnMessage(app_id.ToString(), message); push_service()->OnMessage(app_id.ToString(), message);
ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
EXPECT_EQ("shownotification", script_result); EXPECT_EQ("testdata", script_result);
ASSERT_EQ(1u, notification_manager()->GetNotificationCount()); EXPECT_EQ(0u, notification_manager()->GetNotificationCount());
EXPECT_EQ(base::ASCIIToUTF16("push_test_tag"),
notification_manager()->GetNotificationAt(0).replace_id());
notification_manager()->CancelAll();
ASSERT_EQ(0u, notification_manager()->GetNotificationCount());
// However if the Service Worker push event handler does not show a
// notification, we should show a forced one.
message.data["data"] = "testdata"; message.data["data"] = "testdata";
push_service()->OnMessage(app_id.ToString(), message); push_service()->OnMessage(app_id.ToString(), message);
ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
EXPECT_EQ("testdata", script_result); EXPECT_EQ("testdata", script_result);
ASSERT_EQ(1u, notification_manager()->GetNotificationCount()); EXPECT_EQ(1u, notification_manager()->GetNotificationCount());
EXPECT_EQ(base::ASCIIToUTF16(kPushMessagingForcedNotificationTag), EXPECT_EQ(base::ASCIIToUTF16(kPushMessagingForcedNotificationTag),
notification_manager()->GetNotificationAt(0).replace_id()); notification_manager()->GetNotificationAt(0).replace_id());
...@@ -541,7 +536,31 @@ IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest, ...@@ -541,7 +536,31 @@ IN_PROC_BROWSER_TEST_F(PushMessagingBrowserTest,
push_service()->OnMessage(app_id.ToString(), message); push_service()->OnMessage(app_id.ToString(), message);
ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents)); ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
EXPECT_EQ("shownotification", script_result); EXPECT_EQ("shownotification", script_result);
ASSERT_EQ(2u, notification_manager()->GetNotificationCount()); EXPECT_EQ(2u, notification_manager()->GetNotificationCount());
notification_manager()->CancelAll();
EXPECT_EQ(0u, notification_manager()->GetNotificationCount());
// However if the Service Worker push event handler shows a notification, we
// should not show a forced one.
message.data["data"] = "shownotification";
for (int n = 0; n < 9; n++) {
push_service()->OnMessage(app_id.ToString(), message);
ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
EXPECT_EQ("shownotification", script_result);
EXPECT_EQ(1u, notification_manager()->GetNotificationCount());
EXPECT_EQ(base::ASCIIToUTF16("push_test_tag"),
notification_manager()->GetNotificationAt(0).replace_id());
notification_manager()->CancelAll();
}
// Now that 10 push messages in a row have shown notifications, we should
// allow the next one to mistakenly not show a notification.
message.data["data"] = "testdata";
push_service()->OnMessage(app_id.ToString(), message);
ASSERT_TRUE(RunScript("resultQueue.pop()", &script_result, web_contents));
EXPECT_EQ("testdata", script_result);
EXPECT_EQ(0u, notification_manager()->GetNotificationCount());
} }
#endif #endif
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#include "chrome/browser/services/gcm/push_messaging_service_impl.h" #include "chrome/browser/services/gcm/push_messaging_service_impl.h"
#include <bitset>
#include <vector> #include <vector>
#include "base/bind.h" #include "base/bind.h"
...@@ -29,6 +30,8 @@ ...@@ -29,6 +30,8 @@
#include "components/pref_registry/pref_registry_syncable.h" #include "components/pref_registry/pref_registry_syncable.h"
#include "content/public/browser/browser_context.h" #include "content/public/browser/browser_context.h"
#include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_frame_host.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "content/public/common/child_process_host.h" #include "content/public/common/child_process_host.h"
#include "content/public/common/content_switches.h" #include "content/public/common/content_switches.h"
...@@ -250,78 +253,136 @@ void PushMessagingServiceImpl::RequireUserVisibleUX( ...@@ -250,78 +253,136 @@ void PushMessagingServiceImpl::RequireUserVisibleUX(
// is supported. // is supported.
int notification_count = notification_service->GetNotificationUIManager()-> int notification_count = notification_service->GetNotificationUIManager()->
GetAllIdsByProfileAndSourceOrigin(profile_, application_id.origin).size(); GetAllIdsByProfileAndSourceOrigin(profile_, application_id.origin).size();
if (notification_count > 0) // TODO(johnme): Hiding an existing notification should also count as a useful
return; // user-visible action done in response to a push message - but make sure that
// sending two messages in rapid succession which show then hide a
// Sites with a currently visible tab don't need to show notifications. // notification doesn't count.
bool notification_shown = notification_count > 0;
bool notification_needed = true;
if (!notification_shown) {
// Sites with a currently visible tab don't need to show notifications.
#if defined(OS_ANDROID) #if defined(OS_ANDROID)
for (auto it = TabModelList::begin(); it != TabModelList::end(); ++it) { for (auto it = TabModelList::begin(); it != TabModelList::end(); ++it) {
Profile* profile = (*it)->GetProfile(); Profile* profile = (*it)->GetProfile();
content::WebContents* active_web_contents = content::WebContents* active_web_contents =
(*it)->GetActiveWebContents(); (*it)->GetActiveWebContents();
#else #else
for (chrome::BrowserIterator it; !it.done(); it.Next()) { for (chrome::BrowserIterator it; !it.done(); it.Next()) {
Profile* profile = it->profile(); Profile* profile = it->profile();
content::WebContents* active_web_contents = content::WebContents* active_web_contents =
it->tab_strip_model()->GetActiveWebContents(); it->tab_strip_model()->GetActiveWebContents();
#endif #endif
if (!active_web_contents) if (!active_web_contents)
continue; continue;
// Don't leak information from other profiles. // Don't leak information from other profiles.
if (profile != profile_) if (profile != profile_)
continue; continue;
// Ignore minimized windows etc. // Ignore minimized windows etc.
switch (active_web_contents->GetMainFrame()->GetVisibilityState()) { switch (active_web_contents->GetMainFrame()->GetVisibilityState()) {
case blink::WebPageVisibilityStateHidden: case blink::WebPageVisibilityStateHidden:
case blink::WebPageVisibilityStatePrerender: case blink::WebPageVisibilityStatePrerender:
continue; continue;
case blink::WebPageVisibilityStateVisible: case blink::WebPageVisibilityStateVisible:
break; break;
}
// Use the visible URL since that's the one the user is aware of (and it
// doesn't matter whether the page loaded successfully).
const GURL& active_url = active_web_contents->GetVisibleURL();
// Allow https://foo.example.com Service Worker to not show notification
// if an https://bar.example.com tab is visible (and hence might
// conceivably be showing UI in response to the push message); but http://
// doesn't count as the Service Worker can't talk to it, even with
// navigator.connect.
if (application_id.origin.scheme() != active_url.scheme())
continue;
if (net::registry_controlled_domains::SameDomainOrHost(
application_id.origin, active_url,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) {
notification_needed = false;
break;
}
} }
}
// Use the visible URL since that's the one the user is aware of (and it // Don't track push messages that didn't show a notification but were exempt
// doesn't matter whether the page loaded successfully). // from needing to do so.
const GURL& active_url = active_web_contents->GetVisibleURL(); if (notification_shown || notification_needed) {
content::ServiceWorkerContext* service_worker_context =
// Allow https://foo.example.com Service Worker to not show notification if content::BrowserContext::GetStoragePartitionForSite(
// an https://bar.example.com tab is visible (and hence might conceivably profile_, application_id.origin)->GetServiceWorkerContext();
// be showing UI in response to the push message); but http:// doesn't count
// as the Service Worker can't talk to it, even with navigator.connect. PushMessagingService::GetNotificationsShownByLastFewPushes(
if (application_id.origin.scheme() != active_url.scheme()) service_worker_context, application_id.service_worker_registration_id,
continue; base::Bind(&PushMessagingServiceImpl::DidGetNotificationsShown,
if (net::registry_controlled_domains::SameDomainOrHost( weak_factory_.GetWeakPtr(),
application_id.origin, active_url, application_id, notification_shown, notification_needed));
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) {
return;
}
} }
#endif // defined(ENABLE_NOTIFICATIONS)
}
// If we haven't returned yet, the site failed to show a notification, so we static void IgnoreResult(bool unused) {
// will show a generic notification. See https://crbug.com/437277 }
// TODO(johnme): The generic notification should probably automatically close
// itself when the next push message arrives? void PushMessagingServiceImpl::DidGetNotificationsShown(
content::PlatformNotificationData notification_data; const PushMessagingApplicationId& application_id,
// TODO(johnme): Switch to FormatOriginForDisplay from crbug.com/402698 bool notification_shown, bool notification_needed,
notification_data.title = l10n_util::GetStringFUTF16( const std::string& data, bool success, bool not_found) {
IDS_PUSH_MESSAGING_GENERIC_NOTIFICATION_TITLE, content::ServiceWorkerContext* service_worker_context =
base::UTF8ToUTF16(application_id.origin.host())); content::BrowserContext::GetStoragePartitionForSite(
notification_data.direction = profile_, application_id.origin)->GetServiceWorkerContext();
content::PlatformNotificationData::NotificationDirectionLeftToRight;
notification_data.body = // We remember whether the last (up to) 10 pushes showed notifications.
l10n_util::GetStringUTF16(IDS_PUSH_MESSAGING_GENERIC_NOTIFICATION_BODY); const size_t MISSED_NOTIFICATIONS_LENGTH = 10;
notification_data.tag = // data is a string like "0001000", where '0' means shown, and '1' means
base::ASCIIToUTF16(kPushMessagingForcedNotificationTag); // needed but not shown. We manipulate it in bitset form.
notification_data.icon = GURL(); // TODO(johnme): Better icon? std::bitset<MISSED_NOTIFICATIONS_LENGTH> missed_notifications(data);
notification_service->DisplayPersistentNotification(
profile_, bool needed_but_not_shown = notification_needed && !notification_shown;
application_id.service_worker_registration_id,
application_id.origin, // New entries go at the end, and old ones are shifted off the beginning once
SkBitmap() /* icon */, // the history length is exceeded.
notification_data, missed_notifications <<= 1;
content::ChildProcessHost::kInvalidUniqueID /* render_process_id */); missed_notifications[0] = needed_but_not_shown;
#endif std::string updated_data(missed_notifications.
to_string<char, std::string::traits_type, std::string::allocator_type>());
PushMessagingService::SetNotificationsShownByLastFewPushes(
service_worker_context, application_id.service_worker_registration_id,
application_id.origin, updated_data,
base::Bind(&IgnoreResult)); // This is a heuristic; ignore failure.
if (needed_but_not_shown && missed_notifications.count() >= 2) {
// The site failed to show a notification when one was needed, and they have
// already failed once in the previous 10 push messages, so we will show a
// generic notification. See https://crbug.com/437277.
// TODO(johnme): The generic notification should probably automatically
// close itself when the next push message arrives?
content::PlatformNotificationData notification_data;
// TODO(johnme): Switch to FormatOriginForDisplay from crbug.com/402698
notification_data.title = l10n_util::GetStringFUTF16(
IDS_PUSH_MESSAGING_GENERIC_NOTIFICATION_TITLE,
base::UTF8ToUTF16(application_id.origin.host()));
notification_data.direction =
content::PlatformNotificationData::NotificationDirectionLeftToRight;
notification_data.body =
l10n_util::GetStringUTF16(IDS_PUSH_MESSAGING_GENERIC_NOTIFICATION_BODY);
notification_data.tag =
base::ASCIIToUTF16(kPushMessagingForcedNotificationTag);
notification_data.icon = GURL(); // TODO(johnme): Better icon?
PlatformNotificationServiceImpl* notification_service =
PlatformNotificationServiceImpl::GetInstance();
notification_service->DisplayPersistentNotification(
profile_,
application_id.service_worker_registration_id,
application_id.origin,
SkBitmap() /* icon */,
notification_data,
content::ChildProcessHost::kInvalidUniqueID /* render_process_id */);
}
} }
void PushMessagingServiceImpl::OnMessagesDeleted(const std::string& app_id) { void PushMessagingServiceImpl::OnMessagesDeleted(const std::string& app_id) {
......
...@@ -87,6 +87,13 @@ class PushMessagingServiceImpl : public content::PushMessagingService, ...@@ -87,6 +87,13 @@ class PushMessagingServiceImpl : public content::PushMessagingService,
// happened in the background. When they forget to do so, display a default // happened in the background. When they forget to do so, display a default
// notification on their behalf. // notification on their behalf.
void RequireUserVisibleUX(const PushMessagingApplicationId& application_id); void RequireUserVisibleUX(const PushMessagingApplicationId& application_id);
void DidGetNotificationsShown(
const PushMessagingApplicationId& application_id,
bool notification_shown,
bool notification_needed,
const std::string& data,
bool success,
bool not_found);
void RegisterEnd( void RegisterEnd(
const content::PushMessagingService::RegisterCallback& callback, const content::PushMessagingService::RegisterCallback& callback,
......
...@@ -182,6 +182,7 @@ ...@@ -182,6 +182,7 @@
'public/browser/presentation_service_delegate.h', 'public/browser/presentation_service_delegate.h',
'public/browser/profiler_controller.h', 'public/browser/profiler_controller.h',
'public/browser/profiler_subscriber.h', 'public/browser/profiler_subscriber.h',
'public/browser/push_messaging_service.cc',
'public/browser/push_messaging_service.h', 'public/browser/push_messaging_service.h',
'public/browser/quota_permission_context.h', 'public/browser/quota_permission_context.h',
'public/browser/readback_types.h', 'public/browser/readback_types.h',
......
// Copyright 2015 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 "content/public/browser/push_messaging_service.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/public/browser/browser_thread.h"
namespace {
const char kNotificationsShownServiceWorkerKey[] =
"notifications_shown_by_last_few_pushes";
} // namespace
namespace content {
static void CallGetNotificationsShownCallbackFromIO(
const PushMessagingService::GetNotificationsShownCallback& callback,
const std::string& data,
ServiceWorkerStatusCode service_worker_status) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
bool success = service_worker_status == SERVICE_WORKER_OK;
bool not_found = service_worker_status == SERVICE_WORKER_ERROR_NOT_FOUND;
BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
base::Bind(callback, data, success, not_found));
}
static void CallResultCallbackFromIO(
const ServiceWorkerContext::ResultCallback& callback,
ServiceWorkerStatusCode service_worker_status) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
bool success = service_worker_status == SERVICE_WORKER_OK;
BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
base::Bind(callback, success));
}
static void GetNotificationsShownOnIO(
scoped_refptr<ServiceWorkerContextWrapper> service_worker_context_wrapper,
int64 service_worker_registration_id,
const PushMessagingService::GetNotificationsShownCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
service_worker_context_wrapper->context()->storage()->GetUserData(
service_worker_registration_id, kNotificationsShownServiceWorkerKey,
base::Bind(&CallGetNotificationsShownCallbackFromIO, callback));
}
static void SetNotificationsShownOnIO(
scoped_refptr<ServiceWorkerContextWrapper> service_worker_context_wrapper,
int64 service_worker_registration_id, const GURL& origin,
const std::string& data,
const PushMessagingService::ResultCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
service_worker_context_wrapper->context()->storage()->StoreUserData(
service_worker_registration_id, origin,
kNotificationsShownServiceWorkerKey, data,
base::Bind(&CallResultCallbackFromIO, callback));
}
// static
void PushMessagingService::GetNotificationsShownByLastFewPushes(
ServiceWorkerContext* service_worker_context,
int64 service_worker_registration_id,
const GetNotificationsShownCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
scoped_refptr<ServiceWorkerContextWrapper> wrapper =
static_cast<ServiceWorkerContextWrapper*>(service_worker_context);
BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
base::Bind(&GetNotificationsShownOnIO,
wrapper,
service_worker_registration_id,
callback));
}
// static
void PushMessagingService::SetNotificationsShownByLastFewPushes(
ServiceWorkerContext* service_worker_context,
int64 service_worker_registration_id,
const GURL& origin,
const std::string& notifications_shown,
const ResultCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
scoped_refptr<ServiceWorkerContextWrapper> wrapper =
static_cast<ServiceWorkerContextWrapper*>(service_worker_context);
BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
base::Bind(&SetNotificationsShownOnIO,
wrapper,
service_worker_registration_id,
origin,
notifications_shown,
callback));
}
} // namespace content
...@@ -15,15 +15,38 @@ ...@@ -15,15 +15,38 @@
namespace content { namespace content {
class ServiceWorkerContext;
// A push service-agnostic interface that the Push API uses for talking to // A push service-agnostic interface that the Push API uses for talking to
// push messaging services like GCM. Must only be used on the UI thread. // push messaging services like GCM. Must only be used on the UI thread.
class CONTENT_EXPORT PushMessagingService { class CONTENT_EXPORT PushMessagingService {
public: public:
using GetNotificationsShownCallback =
base::Callback<void(const std::string& notifications_shown,
bool success, bool not_found)>;
using ResultCallback = base::Callback<void(bool success)>;
using RegisterCallback = using RegisterCallback =
base::Callback<void(const std::string& /* registration_id */, base::Callback<void(const std::string& /* registration_id */,
PushRegistrationStatus /* status */)>; PushRegistrationStatus /* status */)>;
using UnregisterCallback = base::Callback<void(PushUnregistrationStatus)>; using UnregisterCallback = base::Callback<void(PushUnregistrationStatus)>;
// Provide a storage mechanism to read/write an opaque
// "notifications_shown_by_last_few_pushes" string associated with a Service
// Worker registration. Stored data is deleted when the associated
// registration is deleted.
static void GetNotificationsShownByLastFewPushes(
ServiceWorkerContext* service_worker_context,
int64 service_worker_registration_id,
const GetNotificationsShownCallback& callback);
static void SetNotificationsShownByLastFewPushes(
ServiceWorkerContext* service_worker_context,
int64 service_worker_registration_id,
const GURL& origin,
const std::string& notifications_shown,
const ResultCallback& callback);
virtual ~PushMessagingService() {} virtual ~PushMessagingService() {}
// Returns the absolute URL exposed by the push server where the webapp server // Returns the absolute URL exposed by the push server where the webapp server
......
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