Create a notification builder for MacOS

This will let us reuse the notification creation logic when migrating
to XPC services

BUG=571056

Review-Url: https://codereview.chromium.org/1981293002
Cr-Commit-Position: refs/heads/master@{#395114}
diff --git a/chrome/browser/DEPS b/chrome/browser/DEPS
index 877d390..7f19930 100644
--- a/chrome/browser/DEPS
+++ b/chrome/browser/DEPS
@@ -98,6 +98,7 @@
   "+third_party/WebKit/public/platform/WebLoadingBehaviorFlag.h",
   "+third_party/WebKit/public/platform/WebReferrerPolicy.h",
   "+third_party/WebKit/public/platform/modules/app_banner/WebAppBannerPromptReply.h",
+  "+third_party/WebKit/public/platform/modules/notifications/WebNotificationConstants.h",
   "+third_party/WebKit/public/platform/modules/push_messaging/WebPushPermissionStatus.h",
   "+third_party/WebKit/public/platform/modules/screen_orientation/WebScreenOrientationLockType.h",
   "+third_party/WebKit/public/platform/modules/permissions/permission_status.mojom.h",
diff --git a/chrome/browser/notifications/notification_builder_mac.h b/chrome/browser/notifications/notification_builder_mac.h
new file mode 100644
index 0000000..692e021
--- /dev/null
+++ b/chrome/browser/notifications/notification_builder_mac.h
@@ -0,0 +1,73 @@
+// 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.
+
+#ifndef CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_BUILDER_MAC_H_
+#define CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_BUILDER_MAC_H_
+
+#import <Foundation/Foundation.h>
+
+#include "base/mac/scoped_nsobject.h"
+
+@class NSUserNotification;
+
+namespace notification_builder {
+extern NSString* const kNotificationOrigin;
+extern NSString* const kNotificationId;
+extern NSString* const kNotificationProfileId;
+extern NSString* const kNotificationIncognito;
+}  // notification_builder
+
+// Provides a marshallable way for storing the information required to construct
+// a NSUSerNotification that is to be displayed on the system.
+//
+// A quick example:
+//     base::scoped_nsobject<NotificationBuilder> builder(
+//         [[NotificationBuilder alloc] init]);
+//     [builder setTitle:@"Hello"];
+//
+//     // Build a notification out of the data.
+//     NSUserNotification* notification =
+//         [builder buildUserNotification];
+//
+//     // Serialize a notification out of the data.
+//     NSDictionary* notificationData = [builder buildDictionary];
+//
+//     // Deserialize the |notificationData| in to a new builder.
+//     base::scoped_nsobject<NotificationBuilder> finalBuilder(
+//         [[NotificationBuilder alloc] initWithData:notificationData]);
+@interface NotificationBuilder : NSObject
+
+// Initializes an empty builder.
+- (instancetype)init;
+
+// Initializes a builder by deserializing |data|. The |data| must have been
+// generated by calling the buildDictionary function on another builder
+// instance.
+- (instancetype)initWithDictionary:(NSDictionary*)data;
+
+// Setters
+- (void)setTitle:(NSString*)title;
+- (void)setSubTitle:(NSString*)subTitle;
+- (void)setContextMessage:(NSString*)contextMessage;
+- (void)setIcon:(NSImage*)icon;
+- (void)setButtons:(NSString*)primaryButton
+    secondaryButton:(NSString*)secondaryButton;
+- (void)setTag:(NSString*)tag;
+- (void)setOrigin:(NSString*)origin;
+- (void)setNotificationId:(NSString*)notificationId;
+- (void)setProfileId:(NSString*)profileId;
+- (void)setIncognito:(BOOL)incognito;
+
+// Returns a notification ready to be displayed out of the provided
+// |notificationData|.
+- (NSUserNotification*)buildUserNotification;
+
+// Returns a representation of a notification that can be serialized.
+// Another instance of NotificationBuilder can read this directly and generate
+// a notification out of it via the |buildbuildUserNotification| method.
+- (NSDictionary*)buildDictionary;
+
+@end
+
+#endif  // CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_BUILDER_MAC_H_
diff --git a/chrome/browser/notifications/notification_builder_mac.mm b/chrome/browser/notifications/notification_builder_mac.mm
new file mode 100644
index 0000000..321f9c3
--- /dev/null
+++ b/chrome/browser/notifications/notification_builder_mac.mm
@@ -0,0 +1,214 @@
+// 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_builder_mac.h"
+
+#import <AppKit/AppKit.h>
+
+#include "base/mac/mac_util.h"
+#include "base/mac/scoped_nsobject.h"
+#include "chrome/grit/generated_resources.h"
+#include "ui/base/l10n/l10n_util_mac.h"
+
+namespace {
+
+// Internal builder constants representing the different notification fields
+// They don't need to be exposed outside the builder.
+
+NSString* const kNotificationTitle = @"title";
+NSString* const kNotificationSubTitle = @"subtitle";
+NSString* const kNotificationInformativeText = @"informativeText";
+NSString* const kNotificationImage = @"icon";
+NSString* const kNotificationButtonOne = @"buttonOne";
+NSString* const kNotificationButtonTwo = @"buttonTwo";
+NSString* const kNotificationTag = @"tag";
+
+}  // namespace
+
+namespace notification_builder {
+
+// Exposed constants to include user related data in the notification.
+NSString* const kNotificationOrigin = @"notificationOrigin";
+NSString* const kNotificationId = @"notificationId";
+NSString* const kNotificationProfileId = @"notificationProfileId";
+NSString* const kNotificationIncognito = @"notificationIncognito";
+
+}  // namespace notification_builder
+
+@implementation NotificationBuilder {
+  base::scoped_nsobject<NSMutableDictionary> notificationData_;
+}
+
+- (instancetype)init {
+  if ((self = [super init])) {
+    notificationData_ = [[NSMutableDictionary alloc] init];
+  }
+  return self;
+}
+
+- (instancetype)initWithDictionary:(NSDictionary*)data {
+  if ((self = [super init])) {
+    notificationData_ = [data copy];
+  }
+  return self;
+}
+
+- (void)setTitle:(NSString*)title {
+  if (title.length)
+    [notificationData_ setObject:title forKey:kNotificationTitle];
+}
+
+- (void)setSubTitle:(NSString*)subTitle {
+  if (subTitle.length)
+    [notificationData_ setObject:subTitle forKey:kNotificationSubTitle];
+}
+
+- (void)setContextMessage:(NSString*)contextMessage {
+  if (contextMessage.length)
+    [notificationData_ setObject:contextMessage
+                          forKey:kNotificationInformativeText];
+}
+
+- (void)setIcon:(NSImage*)icon {
+  if (icon)
+    [notificationData_ setObject:icon forKey:kNotificationImage];
+}
+
+- (void)setButtons:(NSString*)primaryButton
+    secondaryButton:(NSString*)secondaryButton {
+  DCHECK(primaryButton.length);
+  [notificationData_ setObject:primaryButton forKey:kNotificationButtonOne];
+  if (secondaryButton.length) {
+    [notificationData_ setObject:secondaryButton forKey:kNotificationButtonTwo];
+  }
+}
+
+- (void)setTag:(NSString*)tag {
+  if (tag.length)
+    [notificationData_ setObject:tag forKey:kNotificationTag];
+}
+
+- (void)setOrigin:(NSString*)origin {
+  if (origin.length)
+    [notificationData_ setObject:origin
+                          forKey:notification_builder::kNotificationOrigin];
+}
+
+- (void)setNotificationId:(NSString*)notificationId {
+  DCHECK(notificationId.length);
+  [notificationData_ setObject:notificationId
+                        forKey:notification_builder::kNotificationId];
+}
+
+- (void)setProfileId:(NSString*)profileId {
+  DCHECK(profileId.length);
+  [notificationData_ setObject:profileId
+                        forKey:notification_builder::kNotificationProfileId];
+}
+
+- (void)setIncognito:(BOOL)incognito {
+  [notificationData_ setObject:[NSNumber numberWithBool:incognito]
+                        forKey:notification_builder::kNotificationIncognito];
+}
+
+- (NSUserNotification*)buildUserNotification {
+  base::scoped_nsobject<NSUserNotification> toast(
+      [[NSUserNotification alloc] init]);
+  [toast setTitle:[notificationData_ objectForKey:kNotificationTitle]];
+  [toast setSubtitle:[notificationData_ objectForKey:kNotificationSubTitle]];
+  [toast setInformativeText:[notificationData_
+                                objectForKey:kNotificationInformativeText]];
+
+  // Icon
+  if ([notificationData_ objectForKey:kNotificationImage]) {
+    if ([toast respondsToSelector:@selector(_identityImage)]) {
+      NSImage* image = [notificationData_ objectForKey:kNotificationImage];
+      [toast setValue:image forKey:@"_identityImage"];
+      [toast setValue:@NO forKey:@"_identityImageHasBorder"];
+    }
+  }
+
+  // Buttons
+  if ([toast respondsToSelector:@selector(_showsButtons)]) {
+    [toast setValue:@YES forKey:@"_showsButtons"];
+    // A default close button label is provided by the platform but we
+    // explicitly override it in case the user decides to not
+    // use the OS language in Chrome.
+    [toast setOtherButtonTitle:l10n_util::GetNSString(
+                                   IDS_NOTIFICATION_BUTTON_CLOSE)];
+
+    // Display the Settings button as the action button if there are either no
+    // developer-provided action buttons, or the alternate action menu is not
+    // available on this Mac version. This avoids needlessly showing the menu.
+    // TODO(miguelg): Extensions should not have a settings button.
+    if (![notificationData_ objectForKey:kNotificationButtonOne] ||
+        ![toast respondsToSelector:@selector(_alwaysShowAlternateActionMenu)]) {
+      [toast setActionButtonTitle:l10n_util::GetNSString(
+                                      IDS_NOTIFICATION_BUTTON_SETTINGS)];
+    } else {
+      // Otherwise show the alternate menu, then show the developer actions and
+      // finally the settings one.
+      DCHECK(
+          [toast respondsToSelector:@selector(_alwaysShowAlternateActionMenu)]);
+      DCHECK(
+          [toast respondsToSelector:@selector(_alternateActionButtonTitles)]);
+      [toast setActionButtonTitle:l10n_util::GetNSString(
+                                      IDS_NOTIFICATION_BUTTON_OPTIONS)];
+      [toast setValue:@YES forKey:@"_alwaysShowAlternateActionMenu"];
+
+      NSMutableArray* buttons = [NSMutableArray arrayWithCapacity:3];
+      [buttons
+          addObject:[notificationData_ objectForKey:kNotificationButtonOne]];
+      if ([notificationData_ objectForKey:kNotificationButtonTwo]) {
+        [buttons
+            addObject:[notificationData_ objectForKey:kNotificationButtonTwo]];
+      }
+      [buttons
+          addObject:l10n_util::GetNSString(IDS_NOTIFICATION_BUTTON_SETTINGS)];
+      [toast setValue:buttons forKey:@"_alternateActionButtonTitles"];
+    }
+  }
+
+  // Tag
+  if ([toast respondsToSelector:@selector(setIdentifier:)] &&
+      [notificationData_ objectForKey:kNotificationTag]) {
+    [toast setValue:[notificationData_ objectForKey:kNotificationTag]
+             forKey:@"identifier"];
+  }
+
+  NSString* origin =
+      [notificationData_ objectForKey:notification_builder::kNotificationOrigin]
+          ? [notificationData_
+                objectForKey:notification_builder::kNotificationOrigin]
+          : @"";
+  DCHECK(
+      [notificationData_ objectForKey:notification_builder::kNotificationId]);
+  NSString* notificationId =
+      [notificationData_ objectForKey:notification_builder::kNotificationId];
+
+  DCHECK([notificationData_
+      objectForKey:notification_builder::kNotificationProfileId]);
+  NSString* profileId = [notificationData_
+      objectForKey:notification_builder::kNotificationProfileId];
+
+  DCHECK([notificationData_
+      objectForKey:notification_builder::kNotificationIncognito]);
+  NSNumber* incognito = [notificationData_
+      objectForKey:notification_builder::kNotificationIncognito];
+
+  toast.get().userInfo = @{
+    notification_builder::kNotificationOrigin : origin,
+    notification_builder::kNotificationId : notificationId,
+    notification_builder::kNotificationProfileId : profileId,
+    notification_builder::kNotificationIncognito : incognito,
+  };
+
+  return toast.autorelease();
+}
+
+- (NSDictionary*)buildDictionary {
+  return [[notificationData_ copy] autorelease];
+}
+
+@end
diff --git a/chrome/browser/notifications/notification_builder_mac_unittest.mm b/chrome/browser/notifications/notification_builder_mac_unittest.mm
new file mode 100644
index 0000000..0737f33
--- /dev/null
+++ b/chrome/browser/notifications/notification_builder_mac_unittest.mm
@@ -0,0 +1,149 @@
+// 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.
+
+#import <AppKit/AppKit.h>
+
+#include "base/mac/foundation_util.h"
+#include "base/mac/scoped_nsobject.h"
+#include "base/strings/sys_string_conversions.h"
+#include "chrome/browser/notifications/notification_builder_mac.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+TEST(NotificationBuilderMacTest, TestNotificationNoButtons) {
+  base::scoped_nsobject<NotificationBuilder> builder(
+      [[NotificationBuilder alloc] init]);
+  [builder setTitle:@"Title"];
+  [builder setSubTitle:@""];
+  [builder setContextMessage:@"https://www.miguel.com"];
+  [builder setTag:@"tag1"];
+  [builder setIcon:[NSImage imageNamed:@"NSApplicationIcon"]];
+  [builder setNotificationId:@"notificationId"];
+  [builder setProfileId:@"profileId"];
+  [builder setIncognito:false];
+
+  NSUserNotification* notification = [builder buildUserNotification];
+  EXPECT_EQ("Title", base::SysNSStringToUTF8([notification title]));
+  EXPECT_EQ(nullptr, [notification subtitle]);
+  EXPECT_EQ("https://www.miguel.com",
+            base::SysNSStringToUTF8([notification informativeText]));
+  EXPECT_EQ("tag1",
+            base::SysNSStringToUTF8([notification valueForKey:@"identifier"]));
+
+  EXPECT_TRUE([notification hasActionButton]);
+  EXPECT_EQ("Settings",
+            base::SysNSStringToUTF8([notification actionButtonTitle]));
+  EXPECT_EQ("Close", base::SysNSStringToUTF8([notification otherButtonTitle]));
+}
+
+TEST(NotificationBuilderMacTest, TestNotificationOneButton) {
+  base::scoped_nsobject<NotificationBuilder> builder(
+      [[NotificationBuilder alloc] init]);
+  [builder setTitle:@"Title"];
+  [builder setSubTitle:@"SubTitle"];
+  [builder setContextMessage:@"https://www.miguel.com"];
+  [builder setButtons:@"Button1" secondaryButton:@""];
+  [builder setNotificationId:@"notificationId"];
+  [builder setProfileId:@"profileId"];
+  [builder setIncognito:false];
+
+   NSUserNotification* notification = [builder buildUserNotification];
+
+  EXPECT_EQ("Title", base::SysNSStringToUTF8([notification title]));
+  EXPECT_EQ("SubTitle", base::SysNSStringToUTF8([notification subtitle]));
+  EXPECT_EQ("https://www.miguel.com",
+            base::SysNSStringToUTF8([notification informativeText]));
+
+  EXPECT_TRUE([notification hasActionButton]);
+
+  EXPECT_EQ("Options",
+            base::SysNSStringToUTF8([notification actionButtonTitle]));
+  EXPECT_EQ("Close", base::SysNSStringToUTF8([notification otherButtonTitle]));
+
+  NSArray* buttons = [notification valueForKey:@"_alternateActionButtonTitles"];
+  ASSERT_EQ(2u, buttons.count);
+  EXPECT_EQ("Button1", base::SysNSStringToUTF8([buttons objectAtIndex:0]));
+  EXPECT_EQ("Settings", base::SysNSStringToUTF8([buttons objectAtIndex:1]));
+}
+
+TEST(NotificationBuilderMacTest, TestNotificationTwoButtons) {
+  base::scoped_nsobject<NotificationBuilder> builder(
+      [[NotificationBuilder alloc] init]);
+  [builder setTitle:@"Title"];
+  [builder setSubTitle:@"SubTitle"];
+  [builder setContextMessage:@"https://www.miguel.com"];
+  [builder setButtons:@"Button1" secondaryButton:@"Button2"];
+  [builder setNotificationId:@"notificationId"];
+  [builder setProfileId:@"profileId"];
+  [builder setIncognito:false];
+
+  NSUserNotification* notification = [builder buildUserNotification];
+
+  EXPECT_EQ("Title", base::SysNSStringToUTF8([notification title]));
+  EXPECT_EQ("SubTitle", base::SysNSStringToUTF8([notification subtitle]));
+  EXPECT_EQ("https://www.miguel.com",
+            base::SysNSStringToUTF8([notification informativeText]));
+
+  EXPECT_TRUE([notification hasActionButton]);
+
+  EXPECT_EQ("Options",
+            base::SysNSStringToUTF8([notification actionButtonTitle]));
+  EXPECT_EQ("Close", base::SysNSStringToUTF8([notification otherButtonTitle]));
+
+  NSArray* buttons = [notification valueForKey:@"_alternateActionButtonTitles"];
+  ASSERT_EQ(3u, buttons.count);
+  EXPECT_EQ("Button1", base::SysNSStringToUTF8([buttons objectAtIndex:0]));
+  EXPECT_EQ("Button2", base::SysNSStringToUTF8([buttons objectAtIndex:1]));
+  EXPECT_EQ("Settings", base::SysNSStringToUTF8([buttons objectAtIndex:2]));
+}
+
+TEST(NotificationBuilderMacTest, TestUserInfo) {
+  base::scoped_nsobject<NotificationBuilder> builder(
+      [[NotificationBuilder alloc] init]);
+  [builder setTitle:@"Title"];
+  [builder setProfileId:@"Profile1"];
+  [builder setOrigin:@"https://www.miguel.com"];
+  [builder setNotificationId:@"Notification1"];
+  [builder setIncognito:true];
+
+  NSUserNotification* notification = [builder buildUserNotification];
+  EXPECT_EQ("Title", base::SysNSStringToUTF8([notification title]));
+
+  NSDictionary* userInfo = [notification userInfo];
+
+  EXPECT_EQ("https://www.miguel.com",
+            base::SysNSStringToUTF8([userInfo
+                objectForKey:notification_builder::kNotificationOrigin]));
+  EXPECT_EQ("Notification1",
+            base::SysNSStringToUTF8(
+                [userInfo objectForKey:notification_builder::kNotificationId]));
+  EXPECT_EQ("Profile1",
+            base::SysNSStringToUTF8([userInfo
+                objectForKey:notification_builder::kNotificationProfileId]));
+  EXPECT_TRUE([[userInfo
+      objectForKey:notification_builder::kNotificationIncognito] boolValue]);
+}
+
+TEST(NotificationBuilderMacTest, TestBuildDictionary) {
+  NSDictionary* notificationData;
+  {
+    base::scoped_nsobject<NotificationBuilder> sourceBuilder(
+        [[NotificationBuilder alloc] init]);
+    [sourceBuilder setTitle:@"Title"];
+    [sourceBuilder setSubTitle:@"SubTitle"];
+    [sourceBuilder setContextMessage:@"https://www.miguel.com"];
+    [sourceBuilder setNotificationId:@"notificationId"];
+    [sourceBuilder setProfileId:@"profileId"];
+    [sourceBuilder setIncognito:false];
+    notificationData = [sourceBuilder buildDictionary];
+  }
+  base::scoped_nsobject<NotificationBuilder> finalBuilder(
+      [[NotificationBuilder alloc] initWithDictionary:notificationData]);
+
+  NSUserNotification* notification = [finalBuilder buildUserNotification];
+
+  EXPECT_EQ("Title", base::SysNSStringToUTF8([notification title]));
+  EXPECT_EQ("SubTitle", base::SysNSStringToUTF8([notification subtitle]));
+  EXPECT_EQ("https://www.miguel.com",
+            base::SysNSStringToUTF8([notification informativeText]));
+}
diff --git a/chrome/browser/notifications/notification_platform_bridge_mac.mm b/chrome/browser/notifications/notification_platform_bridge_mac.mm
index 8faed66..c80a191 100644
--- a/chrome/browser/notifications/notification_platform_bridge_mac.mm
+++ b/chrome/browser/notifications/notification_platform_bridge_mac.mm
@@ -12,12 +12,14 @@
 #include "base/strings/sys_string_conversions.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/notifications/notification.h"
+#include "chrome/browser/notifications/notification_builder_mac.h"
 #include "chrome/browser/notifications/notification_display_service_factory.h"
 #include "chrome/browser/notifications/persistent_notification_delegate.h"
 #include "chrome/browser/notifications/platform_notification_service_impl.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/profiles/profile_manager.h"
 #include "chrome/grit/generated_resources.h"
+#include "third_party/WebKit/public/platform/modules/notifications/WebNotificationConstants.h"
 #include "ui/base/l10n/l10n_util_mac.h"
 #include "url/gurl.h"
 
@@ -40,18 +42,6 @@
 // - Sound names can be implemented by setting soundName in NSUserNotification
 //   NSUserNotificationDefaultSoundName gives you the platform default.
 
-namespace {
-
-// Keys in NSUserNotification.userInfo to map chrome notifications to
-// native ones.
-NSString* const kNotificationOriginKey = @"notification_origin";
-NSString* const kNotificationPersistentIdKey = @"notification_persistent_id";
-
-NSString* const kNotificationProfilePersistentIdKey =
-    @"notification_profile_persistent_id";
-NSString* const kNotificationIncognitoKey = @"notification_incognito";
-
-}  // namespace
 
 // static
 NotificationPlatformBridge* NotificationPlatformBridge::Create() {
@@ -86,10 +76,11 @@
                                             const std::string& profile_id,
                                             bool incognito,
                                             const Notification& notification) {
-  base::scoped_nsobject<NSUserNotification> toast(
-      [[NSUserNotification alloc] init]);
-  [toast setTitle:base::SysUTF16ToNSString(notification.title())];
-  [toast setSubtitle:base::SysUTF16ToNSString(notification.message())];
+  base::scoped_nsobject<NotificationBuilder> builder(
+      [[NotificationBuilder alloc] init]);
+
+  [builder setTitle:base::SysUTF16ToNSString(notification.title())];
+  [builder setSubTitle:base::SysUTF16ToNSString(notification.message())];
 
   // TODO(miguelg): try to elide the origin perhaps See NSString
   // stringWithFormat. It seems that the informativeText font is constant.
@@ -97,62 +88,29 @@
       notification.context_message().empty()
           ? base::SysUTF8ToNSString(notification.origin_url().spec())
           : base::SysUTF16ToNSString(notification.context_message());
-  [toast setInformativeText:informative_text];
 
-  // Some functionality requires private APIs
-  // Icon
-  if ([toast respondsToSelector:@selector(_identityImage)] &&
-      !notification.icon().IsEmpty()) {
-    [toast setValue:notification.icon().ToNSImage() forKey:@"_identityImage"];
-    [toast setValue:@NO forKey:@"_identityImageHasBorder"];
+  [builder setContextMessage:informative_text];
+  if (!notification.icon().IsEmpty()) {
+    [builder setIcon:notification.icon().ToNSImage()];
   }
 
-  // Buttons
-  if ([toast respondsToSelector:@selector(_showsButtons)]) {
-    [toast setValue:@YES forKey:@"_showsButtons"];
-    // A default close button label is provided by the platform but we
-    // explicitly override it in case the user decides to not
-    // use the OS language in Chrome.
-    [toast setOtherButtonTitle:l10n_util::GetNSString(
-                                   IDS_NOTIFICATION_BUTTON_CLOSE)];
-
-    // Display the Settings button as the action button if there either are no
-    // developer-provided action buttons, or the alternate action menu is not
-    // available on this Mac version. This avoids needlessly showing the menu.
-    if (notification.buttons().empty() ||
-        ![toast respondsToSelector:@selector(_alwaysShowAlternateActionMenu)]) {
-      [toast setActionButtonTitle:l10n_util::GetNSString(
-                                      IDS_NOTIFICATION_BUTTON_SETTINGS)];
-    } else {
-      // Otherwise show the alternate menu, then show the developer actions and
-      // finally the settings one.
-      DCHECK(
-          [toast respondsToSelector:@selector(_alwaysShowAlternateActionMenu)]);
-      DCHECK(
-          [toast respondsToSelector:@selector(_alternateActionButtonTitles)]);
-
-      [toast setActionButtonTitle:l10n_util::GetNSString(
-                                      IDS_NOTIFICATION_BUTTON_OPTIONS)];
-      [toast setValue:@YES forKey:@"_alwaysShowAlternateActionMenu"];
-
-      NSMutableArray* buttons = [NSMutableArray arrayWithCapacity:3];
-      for (const auto& action : notification.buttons())
-        [buttons addObject:base::SysUTF16ToNSString(action.title)];
-      [buttons
-          addObject:l10n_util::GetNSString(IDS_NOTIFICATION_BUTTON_SETTINGS)];
-
-      [toast setValue:buttons forKey:@"_alternateActionButtonTitles"];
-    }
+  std::vector<message_center::ButtonInfo> buttons = notification.buttons();
+  if (!buttons.empty()) {
+    DCHECK_LE(buttons.size(), blink::kWebNotificationMaxActions);
+    NSString* buttonOne = SysUTF16ToNSString(buttons[0].title);
+    NSString* buttonTwo = nullptr;
+    if (buttons.size() > 1)
+      buttonTwo = SysUTF16ToNSString(buttons[1].title);
+    [builder setButtons:buttonOne secondaryButton:buttonTwo];
   }
 
   // Tag
-  if ([toast respondsToSelector:@selector(setIdentifier:)] &&
-      !notification.tag().empty()) {
-    [toast setValue:base::SysUTF8ToNSString(notification.tag())
-             forKey:@"identifier"];
-
+  if (!notification.tag().empty()) {
+    [builder setTag:base::SysUTF8ToNSString(notification.tag())];
     // If renotify is needed, delete the notification with the same tag
     // from the notification center before displaying this one.
+    // TODO(miguelg): This will need to work for alerts as well via XPC
+    // once supported.
     if (notification.renotify()) {
       NSUserNotificationCenter* notification_center =
           [NSUserNotificationCenter defaultUserNotificationCenter];
@@ -169,13 +127,12 @@
     }
   }
 
-  toast.get().userInfo = @{
-    kNotificationOriginKey :
-        base::SysUTF8ToNSString(notification.origin_url().spec()),
-    kNotificationPersistentIdKey : base::SysUTF8ToNSString(notification_id),
-    kNotificationProfilePersistentIdKey : base::SysUTF8ToNSString(profile_id),
-    kNotificationIncognitoKey : [NSNumber numberWithBool:incognito]
-  };
+  [builder setOrigin:base::SysUTF8ToNSString(notification.origin_url().spec())];
+  [builder setNotificationId:base::SysUTF8ToNSString(notification_id)];
+  [builder setProfileId:base::SysUTF8ToNSString(profile_id)];
+  [builder setIncognito:incognito];
+
+  NSUserNotification* toast = [builder buildUserNotification];
 
   [notification_center_ deliverNotification:toast];
 }
@@ -188,10 +145,10 @@
   for (NSUserNotification* toast in
        [notification_center_ deliveredNotifications]) {
     NSString* toast_id =
-        [toast.userInfo objectForKey:kNotificationPersistentIdKey];
+        [toast.userInfo objectForKey:notification_builder::kNotificationId];
 
-    NSString* persistent_profile_id =
-        [toast.userInfo objectForKey:kNotificationProfilePersistentIdKey];
+    NSString* persistent_profile_id = [toast.userInfo
+        objectForKey:notification_builder::kNotificationProfileId];
 
     if (toast_id == candidate_id &&
         persistent_profile_id == current_profile_id) {
@@ -208,11 +165,11 @@
   NSString* current_profile_id = base::SysUTF8ToNSString(profile_id);
   for (NSUserNotification* toast in
        [notification_center_ deliveredNotifications]) {
-    NSString* toast_profile_id =
-        [toast.userInfo objectForKey:kNotificationProfilePersistentIdKey];
+    NSString* toast_profile_id = [toast.userInfo
+        objectForKey:notification_builder::kNotificationProfileId];
     if (toast_profile_id == current_profile_id) {
       notifications->insert(base::SysNSStringToUTF8(
-          [toast.userInfo objectForKey:kNotificationPersistentIdKey]));
+          [toast.userInfo objectForKey:notification_builder::kNotificationId]));
     }
   }
   return true;
@@ -227,14 +184,15 @@
 @implementation NotificationCenterDelegate
 - (void)userNotificationCenter:(NSUserNotificationCenter*)center
        didActivateNotification:(NSUserNotification*)notification {
-  std::string notificationOrigin = base::SysNSStringToUTF8(
-      [notification.userInfo objectForKey:kNotificationOriginKey]);
-  NSNumber* persistentNotificationId =
-      [notification.userInfo objectForKey:kNotificationPersistentIdKey];
-  NSString* persistentProfileId =
-      [notification.userInfo objectForKey:kNotificationProfilePersistentIdKey];
-  NSNumber* isIncognito =
-      [notification.userInfo objectForKey:kNotificationIncognitoKey];
+  std::string notificationOrigin =
+      base::SysNSStringToUTF8([notification.userInfo
+          objectForKey:notification_builder::kNotificationOrigin]);
+  NSNumber* notificationId = [notification.userInfo
+      objectForKey:notification_builder::kNotificationId];
+  NSString* profileId = [notification.userInfo
+      objectForKey:notification_builder::kNotificationProfileId];
+  NSNumber* isIncognito = [notification.userInfo
+      objectForKey:notification_builder::kNotificationIncognito];
 
   GURL origin(notificationOrigin);
 
@@ -280,9 +238,9 @@
 
   PlatformNotificationServiceImpl::GetInstance()
       ->ProcessPersistentNotificationOperation(
-          operation, base::SysNSStringToUTF8(persistentProfileId),
-          [isIncognito boolValue], origin,
-          persistentNotificationId.longLongValue, buttonIndex);
+          operation, base::SysNSStringToUTF8(profileId),
+          [isIncognito boolValue], origin, notificationId.longLongValue,
+          buttonIndex);
 }
 
 - (BOOL)userNotificationCenter:(NSUserNotificationCenter*)center
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index 50a3313..b9df5df 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -2191,6 +2191,8 @@
       'browser/notifications/native_notification_display_service.h',
       'browser/notifications/notification.cc',
       'browser/notifications/notification.h',
+      'browser/notifications/notification_builder_mac.mm',
+      'browser/notifications/notification_builder_mac.h',
       'browser/notifications/notification_delegate.h',
       'browser/notifications/notification_display_service.h',
       'browser/notifications/notification_display_service_factory.cc',
diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi
index 59be689..5c3daf5 100644
--- a/chrome/chrome_tests_unit.gypi
+++ b/chrome/chrome_tests_unit.gypi
@@ -558,6 +558,7 @@
       'browser/media/cast_transport_host_filter_unittest.cc',
       'browser/metrics/extensions_metrics_provider_unittest.cc',
       'browser/notifications/extension_welcome_notification_unittest.cc',
+      'browser/notifications/notification_builder_mac_unittest.mm',
       'browser/notifications/notification_conversion_helper_unittest.cc',
       'browser/renderer_context_menu/context_menu_content_type_unittest.cc',
       'browser/search/hotword_service_unittest.cc',