| 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);
|
| +}
|
|
|