Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2783)

Unified Diff: chrome/browser/notifications/notification_platform_bridge_win.cc

Issue 2033093003: [Notification] Make HTML5 Notification use ActionCenter on Windows 10, behind Flags. Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Sync and merge. Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/browser/notifications/notification_platform_bridge_win.cc
diff --git a/chrome/browser/notifications/notification_platform_bridge_win.cc b/chrome/browser/notifications/notification_platform_bridge_win.cc
new file mode 100644
index 0000000000000000000000000000000000000000..7ba33c69d863eea381358a738b3a13728b600891
--- /dev/null
+++ b/chrome/browser/notifications/notification_platform_bridge_win.cc
@@ -0,0 +1,468 @@
+// Copyright 2016 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_platform_bridge_win.h"
+
+#include <stdint.h>
+#include <stdio.h>
+
+#include <algorithm>
+#include <utility>
+
+#include "base/callback.h"
+#include "base/files/file_util.h"
+#include "base/logging.h"
+#include "base/macros.h"
+#include "base/memory/ref_counted_memory.h"
+#include "base/strings/string16.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/task_runner.h"
+#include "base/threading/sequenced_worker_pool.h"
+#include "base/win/windows_version.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/notifications/notification.h"
+#include "chrome/browser/notifications/notification_toast_helper_win.h"
+#include "chrome/browser/notifications/platform_notification_service_impl.h"
+#include "content/public/browser/browser_thread.h"
+#include "skia/ext/image_operations.h"
+#include "third_party/skia/include/core/SkColor.h"
+#include "ui/gfx/canvas.h"
+#include "ui/gfx/geometry/rect_f.h"
+#include "ui/gfx/geometry/size.h"
+#include "ui/gfx/image/image.h"
+#include "ui/gfx/image/image_skia.h"
+#include "ui/message_center/notification_delegate.h"
+
+namespace {
+
+const int kNotificationIconSize = 32;
+const SkColor kNotificationShadowColor = SkColorSetARGB(0.5 * 255, 0, 0, 0);
+
+wchar_t kToastXmlTemplate[] =
+ L"<toast>\n"
+ L" <visual>\n"
+ L" <binding template=\"ToastGeneric\">\n"
+ L" <image placement=\"appLogoOverride\"/>\n"
+ L" <text id=\"1\"></text>\n"
+ L" <text id=\"2\"></text>\n"
+ L" </binding>\n"
+ L" </visual>\n"
+ L" <actions>\n"
+ L" <action activationType=\"background\" content=\"Settings...\" "
+ L"arguments=\"setting\"/>\n"
+ L" </actions>\n"
+ L"</toast>";
+
+// We use em space to pad <text> tag contents. Without this, then whenever
+// <text> is left blank (or all-space), we'd see weird default text, e.g.,
+// "Chrome.(SOME STRING)" and "New notification".
+wchar_t kEmSpace[] = L"\u2003";
+
+wchar_t kAttrDataNotificationId[] = L"data-notification-id";
+wchar_t kAttrDataProfileId[] = L"data-profile-id";
+
+// Shrinks |src_image| if necessary. Draws the resized image to the centre of a
+// square image specified by |new_size| and |background_color|. Returns the
+// result via |dst_image|.
+void FormatImageToFitSquare(const gfx::Image& src_image,
+ int new_size,
+ SkColor background_color,
+ gfx::Image* dst_image) {
+ int src_width = src_image.Width();
+ int src_height = src_image.Height();
+
+ // Shrink image if it won't fit in square; otherwise just copy (don't expand).
+ gfx::ImageSkia resized_image;
+ if (src_width > new_size || src_height > new_size) {
+ int src_max_dim = std::max(src_width, src_height);
+ int resized_width = new_size * src_width / src_max_dim;
+ int resized_height = new_size * src_height / src_max_dim;
+ resized_image = gfx::ImageSkia::CreateFrom1xBitmap(
+ skia::ImageOperations::Resize(
+ *src_image.ToSkBitmap(),
+ skia::ImageOperations::RESIZE_LANCZOS3,
+ resized_width,
+ resized_height));
+ } else {
+ resized_image = src_image.AsImageSkia();
+ }
+
+ // Create square image with |background_color|.
+ gfx::Canvas canvas(gfx::Size(new_size, new_size), 1.0f, false);
+ canvas.DrawRect(gfx::RectF(0, 0, new_size, new_size), background_color);
+
+ // Draw the resized image at center of square image, and return.
+ canvas.DrawImageInt(resized_image, (new_size - resized_image.width()) / 2,
+ (new_size - resized_image.height()) / 2);
+ *dst_image = gfx::Image::CreateFrom1xBitmap(canvas.GetBitmap());
+}
+
+} // namespace
+
+// static
+NotificationPlatformBridge* NotificationPlatformBridge::Create() {
+ return new NotificationPlatformBridgeWin();
+}
+
+// States of a toast notification.
+struct NotificationToastSession :
+ public base::RefCounted<NotificationToastSession> {
+ NotificationToastSession(const std::string& notification_id_in,
+ const std::string& profile_id_in,
+ bool is_incognito_in,
+ const Notification& notification_in)
+ : notification_id(notification_id_in),
+ profile_id(profile_id_in),
+ is_incognito(is_incognito_in),
+ notification(notification_in), // Copy by value.
+ debug_direct_xml(false),
+ has_icon(!notification_in.icon().IsEmpty()) {
+ CHECK(base::win::GetVersion() >= base::win::VERSION_WIN10_R1);
+ }
+
+ ~NotificationToastSession() {
+ // TODO(huangs): See if we can delete earlier, e.g., after brief delay??
+ if (!temp_image_file.empty())
+ DeleteTempImageFileOnFileThread();
+ }
+
+ // Deletes temporary icon on FILE thread: fire and forget.
+ void DeleteTempImageFileOnFileThread() {
+ content::BrowserThread::PostTask(content::BrowserThread::FILE,
+ FROM_HERE,
+ base::Bind(base::IgnoreResult(&base::DeleteFile),
+ temp_image_file,
+ false /* recursive */));
+ temp_image_file.clear();
+ }
+
+ std::string notification_id;
+ std::string profile_id;
+ bool is_incognito;
+ const Notification notification;
+
+ const bool has_icon;
+ gfx::Image formatted_icon;
+
+ bool debug_direct_xml;
+
+ base::FilePath temp_image_file;
+
+ DISALLOW_COPY_AND_ASSIGN(NotificationToastSession);
+};
+
+// This callback is invoked when user clicks on the notification toast. This can
+// occur at any time in Chrome's cycle, so we need to be robust.
+HRESULT NotificationPlatformBridgeWin::ToastEventHandler::OnActivated(
+ winui::Notifications::IToastNotification* notification,
+ IInspectable* /* inspectable */) {
+ PostHandlerOnUIThread(EVENT_TYPE_ACTIVATED, notification);
+ return S_OK;
+}
+
+HRESULT NotificationPlatformBridgeWin::ToastEventHandler::OnDismissed(
+ winui::Notifications::IToastNotification* notification,
+ winui::Notifications::IToastDismissedEventArgs* /* args */) {
+ PostHandlerOnUIThread(EVENT_TYPE_DISMISSED, notification);
+ return S_OK;
+}
+
+HRESULT NotificationPlatformBridgeWin::ToastEventHandler::OnFailed(
+ winui::Notifications::IToastNotification* notification,
+ winui::Notifications::IToastFailedEventArgs* /* args */) {
+ PostHandlerOnUIThread(EVENT_TYPE_FAILED, notification);
+ return S_OK;
+}
+
+// static
+void NotificationPlatformBridgeWin::ToastEventHandler::PostHandlerOnUIThread(
+ EventType type,
+ winui::Notifications::IToastNotification* notification) {
+ // Extract the notifiation ID.
+ NotificationToastHelperWin helper;
+ helper.LoadNotificationAndXml(notification);
+ helper.SelectDocument();
+ std::string notification_id =
+ base::UTF16ToUTF8(helper.GetAttribute(kAttrDataNotificationId));
+ std::string profile_id =
+ base::UTF16ToUTF8(helper.GetAttribute(kAttrDataProfileId));
+
+ // Post task on UI thread.
+ content::BrowserThread::PostTask(content::BrowserThread::UI,
+ FROM_HERE,
+ base::Bind(
+ &NotificationPlatformBridgeWin::ToastEventHandler::HandleOnUIThread,
+ type,
+ notification_id,
+ profile_id));
+}
+
+// static
+void NotificationPlatformBridgeWin::ToastEventHandler::HandleOnUIThread(
+ EventType type,
+ const std::string& notification_id,
+ const std::string& profile_id) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ // Get the NotificationPlatformBridgeWin singleton.
+ if (!g_browser_process)
+ return;
+ NotificationPlatformBridge* notification_bridge =
+ g_browser_process->notification_platform_bridge();
+ if (!notification_bridge)
+ return;
+ NotificationPlatformBridgeWin* notification_bridge_win =
+ static_cast<NotificationPlatformBridgeWin*>(notification_bridge);
+
+ switch (type) {
+ case EVENT_TYPE_ACTIVATED: {
+ notification_bridge_win->OnClickEvent(notification_id, profile_id);
+ notification_bridge_win->OnCloseEvent(notification_id, profile_id);
+ break;
+ }
+ case EVENT_TYPE_DISMISSED: {
+ notification_bridge_win->OnCloseEvent(notification_id, profile_id);
+ break;
+ }
+ case EVENT_TYPE_FAILED: {
+ break;
+ }
+ }
+
+ // Currently we only perform cleanup.
+ notification_bridge_win->CleanupSession(notification_id);
+}
+
+NotificationPlatformBridgeWin::ToastEventHandler
+ NotificationPlatformBridgeWin::toast_event_handler_;
+
+NotificationPlatformBridgeWin::~NotificationPlatformBridgeWin() {}
+
+void NotificationPlatformBridgeWin::Display(
+ NotificationCommon::Type notification_type,
+ const std::string& notification_id,
+ const std::string& profile_id,
+ bool is_incognito,
+ const Notification& notification) {
+ // TODO(huangs): Deal with |type|.
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ if (session_map_.count(notification_id)) // Ignore duplicated session.
+ return;
+ scoped_refptr<NotificationToastSession> session =
+ new NotificationToastSession(
+ notification_id, profile_id, is_incognito, notification);
+ session_map_[notification_id] = session;
+
+ // Debug code: Directly inject XML!! TODO(huangs): Remove.
+ if (session->notification.message().substr(0, 1) == L"<") {
+ session->debug_direct_xml = true;
+ DisplayStepMain(session);
+ return;
+ }
+
+ if (!session->has_icon) {
+ // No icon: Can immediatly display.
+ DisplayStepMain(session);
+ } else {
+ // Has icon: Format image on a worker thread.
+ content::BrowserThread::GetBlockingPool()
+ ->GetTaskRunnerWithShutdownBehavior(
+ base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)->PostTask(FROM_HERE,
+ base::Bind(
+ &NotificationPlatformBridgeWin::DisplayStepFormatIconOnWorkerThread,
+ base::Unretained(this),
+ session));
+ }
+}
+
+void NotificationPlatformBridgeWin::Close(
+ const std::string& profile_id,
+ const std::string& notification_id) {
+ // TODO(huangs): Implement.
+ ::MessageBox(NULL, L"Close()", L"Title", MB_OK);
+}
+
+void NotificationPlatformBridgeWin::GetDisplayed(
+ const std::string& profile_id,
+ bool is_incognito,
+ const GetDisplayedNotificationsCallback& callback) const {
+ // TODO(huangs): Implement.
+}
+
+void NotificationPlatformBridgeWin::SetReadyCallback(
+ NotificationBridgeReadyCallback callback) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ std::move(callback).Run(true);
+}
+
+void NotificationPlatformBridgeWin::DisplayStepFormatIconOnWorkerThread(
+ scoped_refptr<NotificationToastSession> session) {
+ FormatImageToFitSquare(session->notification.icon(),
+ kNotificationIconSize,
+ kNotificationShadowColor,
+ &session->formatted_icon);
+
+ // Save image to a temp file on the FILE thread.
+ content::BrowserThread::PostTask(content::BrowserThread::FILE,
+ FROM_HERE,
+ base::Bind(
+ &NotificationPlatformBridgeWin::DisplayStepPrepareIconOnFileThread,
+ base::Unretained(this),
+ session));
+}
+
+void NotificationPlatformBridgeWin::DisplayStepPrepareIconOnFileThread(
+ scoped_refptr<NotificationToastSession> session) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::FILE);
+
+ // TODO(huangs): Possible alternative?
+ // We should set the image and launch params attribute in the notification
+ // XNL as described here: http://msdn.microsoft.com/en-us/library/hh465448
+ // To set the image we may have to extract the image and specify it in the
+ // following url form. ms-appx:///images/foo.png
+ // The launch params as described don't get passed back to us via the
+ // winapp::Activation::ILaunchActivatedEventArgs argument. Needs to be
+ // investigated.
+
+ // Write icon to temporary file store the filename. On failure, we clear the
+ // filename and allow flow to proceed without icon.
+ const gfx::Image& icon = session->formatted_icon.IsEmpty() ?
+ session->notification.icon() : session->formatted_icon;
+ scoped_refptr<base::RefCountedMemory> png = icon.As1xPNGBytes();
+ base::FilePath temp_file;
+ session->temp_image_file.clear();
+ // Create tempory file, and rename it to have ".png" extension because the API
+ // relies on extension.
+ if (base::CreateTemporaryFile(&temp_file)) {
+ base::FilePath temp_png_file =
+ temp_file.AddExtension(FILE_PATH_LITERAL(".png"));
+ if (base::ReplaceFile(temp_file, temp_png_file, nullptr)) {
+ if (base::WriteFile(temp_png_file,
+ reinterpret_cast<const char *>(png->front()),
+ png->size()) >= 0) {
+ session->temp_image_file = temp_png_file;
+ } else {
+ base::DeleteFile(temp_png_file, false);
+ }
+ } else {
+ base::DeleteFile(temp_file, false);
+ }
+ }
+
+ content::BrowserThread::PostTask(content::BrowserThread::UI,
+ FROM_HERE,
+ base::Bind(&NotificationPlatformBridgeWin::DisplayStepMain,
+ base::Unretained(this),
+ session));
+}
+
+bool NotificationPlatformBridgeWin::DisplayStepMainWorker(
+ scoped_refptr<NotificationToastSession> session) {
+ NotificationToastHelperWin helper;
+ helper.CreateToastManager();
+
+ if (session->debug_direct_xml) {
+ // TODO(huangs): Remove debugging code.
+ helper.LoadXMLFromString(session->notification.message());
+
+ } else {
+ // Load template XML and customize it.
+ helper.LoadXMLFromString(kToastXmlTemplate);
+
+ helper.SelectElementByTagNameAndIndex(L"image", 0);
+ if (session->temp_image_file.empty()) {
+ helper.RemoveElement();
+
+ } else {
+ base::string16 file_url =
+ helper.FilePathToFileUrl(session->temp_image_file);
+ helper.SetAttribute(L"src", file_url);
+ }
+
+ helper.SelectElementByTagNameAndIndex(L"text", 0);
+ helper.AppendText(session->notification.title() + kEmSpace);
+
+ helper.SelectElementByTagNameAndIndex(L"text", 1);
+ helper.AppendText(session->notification.message() + kEmSpace);
+ }
+
+ helper.SelectDocument();
+ helper.SetAttribute(L"duration", L"long");
+ // Store notification ID as part of XML, retrieved later in event handlers.
+ helper.SetAttribute(kAttrDataNotificationId,
+ base::UTF8ToUTF16(session->notification_id).c_str());
+ helper.SetAttribute(kAttrDataProfileId,
+ base::UTF8ToUTF16(session->profile_id).c_str());
+
+ helper.AlertToastXml(); // Debug code.
+ helper.CreateToastNotification();
+ helper.CreateToastNotifier();
+
+ if (helper.HasFailed())
+ return false;
+
+ auto activated_handler = mswr::Callback<ToastActivatedHandler>(
+ &toast_event_handler_,
+ &NotificationPlatformBridgeWin::ToastEventHandler::OnActivated);
+ auto dismissed_handler = mswr::Callback<ToastDismissedHandler>(
+ &toast_event_handler_,
+ &NotificationPlatformBridgeWin::ToastEventHandler::OnDismissed);
+ auto failed_handler = mswr::Callback<ToastFailedHandler>(
+ &toast_event_handler_,
+ &NotificationPlatformBridgeWin::ToastEventHandler::OnFailed);
+
+ // TODO(huangs): Pass these to |session|, and unsubscribe using these.
+ EventRegistrationToken activated_token;
+ EventRegistrationToken dismissed_token;
+ EventRegistrationToken failed_token;
+
+ helper.Show(activated_handler, dismissed_handler, failed_handler,
+ &activated_token, &dismissed_token, &failed_token);
+ return helper.StillOkay();
+}
+
+void NotificationPlatformBridgeWin::DisplayStepMain(
+ scoped_refptr<NotificationToastSession> session) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+ bool success = DisplayStepMainWorker(session);
+
+ // On failure, clean up right away.
+ if (!success)
+ CleanupSession(session->notification_id);
+ // On success, clean up will happen via Windows callback to event handlers.
+}
+
+void NotificationPlatformBridgeWin::OnClickEvent(
+ const std::string& notification_id,
+ const std::string& profile_id) {
+ if (!session_map_.count(notification_id))
+ return;
+ scoped_refptr<NotificationToastSession> session =
+ session_map_[notification_id];
+ DCHECK(session->notification_id == notification_id);
+ DCHECK(session->profile_id == profile_id);
+
+ session->notification.delegate()->Click();
+ // session->notification.delegate()->SettingsClick();
+}
+
+void NotificationPlatformBridgeWin::OnCloseEvent(
+ const std::string& notification_id,
+ const std::string& profile_id) {
+ if (!session_map_.count(notification_id))
+ return;
+ scoped_refptr<NotificationToastSession> session =
+ session_map_[notification_id];
+ DCHECK(session->notification_id == notification_id);
+ DCHECK(session->profile_id == profile_id);
+
+ // TODO(huangs): Distinguish user close vs. timeout close.
+ session->notification.delegate()->Close(true /* by_user */);
+}
+
+void NotificationPlatformBridgeWin::CleanupSession(
+ std::string notification_id) {
+ session_map_.erase(notification_id);
+}

Powered by Google App Engine
This is Rietveld 408576698