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