Introduce alert notification helper .app

This adds a new helper .app on macOS to display alert notifications.
This app is required to show alert style notifications as the main app
can only show banner style ones and the XPC service can not use the new
UNNotification APIs.

Bug: 1127306
Change-Id: Ie820978824fcfabbb104ea11daba2c415e60205b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2648046
Commit-Queue: Richard Knoll <knollr@chromium.org>
Reviewed-by: Dirk Pranke <dpranke@google.com>
Reviewed-by: Robert Sesek <rsesek@chromium.org>
Reviewed-by: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#860254}
diff --git a/chrome/BUILD.gn b/chrome/BUILD.gn
index 6f386508..35f7bac4 100644
--- a/chrome/BUILD.gn
+++ b/chrome/BUILD.gn
@@ -560,15 +560,20 @@
     }
   }
 
+  bundle_data("chrome_app_icon") {
+    sources = [ "app/theme/$branding_path_component/mac/app.icns" ]
+    outputs = [ "{{bundle_resources_dir}}/{{source_file_part}}" ]
+  }
+
   bundle_data("chrome_resources") {
     sources = [
       "$root_out_dir/$chrome_mac_bundle_id.manifest",
-      "app/theme/$branding_path_component/mac/app.icns",
       "app/theme/$branding_path_component/mac/document.icns",
       "browser/ui/cocoa/applescript/scripting.sdef",
     ]
     outputs = [ "{{bundle_resources_dir}}/{{source_file_part}}" ]
     public_deps = [
+      ":chrome_app_icon",
       ":chrome_app_strings",
       "//components/policy:chrome_manifest_bundle",
     ]
@@ -649,7 +654,12 @@
 
       output_name = chrome_helper_name + invoker.helper_name_suffix
 
-      info_plist_target = ":chrome_helper_plist"
+      if (defined(invoker.info_plist_target)) {
+        info_plist_target = invoker.info_plist_target
+      } else {
+        info_plist_target = ":chrome_helper_plist"
+      }
+
       extra_substitutions = [
         "CHROMIUM_BUNDLE_ID=$chrome_mac_bundle_id",
         "CHROMIUM_SHORT_NAME=$chrome_product_short_name",
@@ -668,6 +678,10 @@
         "//sandbox/mac:seatbelt",
       ]
 
+      if (defined(invoker.deps)) {
+        deps += invoker.deps
+      }
+
       ldflags = []
 
       if (is_component_build) {
@@ -690,16 +704,77 @@
     }
   }
 
-  foreach(helper_params, content_mac_helpers) {
-    _helper_target = helper_params[0]
-    _helper_bundle_id = helper_params[1]
-    _helper_suffix = helper_params[2]
-    chrome_helper_app("chrome_helper_app_${_helper_target}") {
-      helper_name_suffix = _helper_suffix
-      helper_bundle_id_suffix = _helper_bundle_id
-    }
+  # The following *_helper_params are added to the ones provided by //content
+  # listed in content_mac_helpers (see //content/public/app/mac_helpers.gni).
+  # These allow //chrome to add custom helper apps in addition to the ones
+  # provided by //content. The params here have the same form as the content
+  # helpers and are defined as a tuple of these elements:
+  #   target name - A short name to be used when defining the target for that
+  #                 helper variant.
+  #   bundle ID suffix - A string fragment to append to the CFBundleIdentifier of
+  #                      the helper.
+  #   app name suffix - A string fragment to append to the outer bundle name as
+  #                     well as the inner executable. This should be reflected in
+  #                     the target's output_name.
 
-    if (verify_dynamic_libraries) {
+  # Helper app to display alert notifications. This is necessary as an app can
+  # only display either banner or alert style notifications and the main app
+  # will display banners.
+  alert_helper_params = [
+    "alerts",
+    ".alerts",
+    " (Alerts)",
+  ]
+
+  # Merge all helper apps needed by //content and //chrome.
+  chrome_mac_helpers = content_mac_helpers + [ alert_helper_params ]
+
+  # Create all helper apps required by //content.
+  foreach(helper_params, content_mac_helpers) {
+    chrome_helper_app("chrome_helper_app_${helper_params[0]}") {
+      helper_name_suffix = helper_params[2]
+      helper_bundle_id_suffix = helper_params[1]
+    }
+  }
+
+  # Create app for the alert helper manually here as we want to modify the plist
+  # to set the alert style and add the app icon to its resources.
+  tweak_info_plist("chrome_helper_app_alerts_plist") {
+    deps = [ ":chrome_helper_plist" ]
+    info_plists = get_target_outputs(":chrome_helper_plist") +
+                  [ "app/helper-alerts-Info.plist" ]
+  }
+
+  # Create and bundle an InfoPlist.strings for the alert helper app.
+  # TODO(crbug.com/1182393): Disambiguate and localize alert helper app name.
+  compile_plist("chrome_helper_app_alerts_plist_strings") {
+    format = "binary1"
+    plist_templates = [ "app/helper-alerts-InfoPlist.strings" ]
+    substitutions = [ "CHROMIUM_FULL_NAME=$chrome_product_full_name" ]
+    output_name = "$target_gen_dir/helper_alerts_infoplist_strings/base.lproj/InfoPlist.strings"
+  }
+  bundle_data("chrome_helper_app_alerts_resources") {
+    sources = get_target_outputs(":chrome_helper_app_alerts_plist_strings")
+    outputs = [ "{{bundle_resources_dir}}/base.lproj/{{source_file_part}}" ]
+    public_deps = [ ":chrome_helper_app_alerts_plist_strings" ]
+  }
+
+  chrome_helper_app("chrome_helper_app_${alert_helper_params[0]}") {
+    helper_name_suffix = alert_helper_params[2]
+    helper_bundle_id_suffix = alert_helper_params[1]
+    info_plist_target = ":chrome_helper_app_alerts_plist"
+    deps = [
+      ":chrome_app_icon",
+      ":chrome_helper_app_alerts_resources",
+    ]
+  }
+
+  if (verify_dynamic_libraries) {
+    foreach(helper_params, chrome_mac_helpers) {
+      _helper_target = helper_params[0]
+      _helper_bundle_id = helper_params[1]
+      _helper_suffix = helper_params[2]
+
       action("verify_libraries_chrome_helper_app_${_helper_target}") {
         script = "//chrome/tools/build/mac/verify_dynamic_libraries.py"
         inputs = [ "${root_out_dir}/${chrome_helper_name}${_helper_suffix}.app/Contents/MacOS/${chrome_helper_name}${_helper_suffix}" ]
@@ -737,7 +812,7 @@
       "//components/crash/core/app:chrome_crashpad_handler",
     ]
 
-    foreach(helper_params, content_mac_helpers) {
+    foreach(helper_params, chrome_mac_helpers) {
       sources +=
           [ "$root_out_dir/${chrome_helper_name}${helper_params[2]}.app" ]
       public_deps += [ ":chrome_helper_app_${helper_params[0]}" ]
@@ -1119,7 +1194,7 @@
       _framework_binary_path,
     ]
 
-    foreach(helper_params, content_mac_helpers) {
+    foreach(helper_params, chrome_mac_helpers) {
       _chrome_symbols_sources += [ "$root_out_dir/${chrome_helper_name}${helper_params[2]}.app/Contents/MacOS/${chrome_helper_name}${helper_params[2]}" ]
     }
 
@@ -1153,7 +1228,7 @@
         "//third_party/swiftshader/src/OpenGL/libGLESv2:swiftshader_libGLESv2",
       ]
 
-      foreach(helper_params, content_mac_helpers) {
+      foreach(helper_params, chrome_mac_helpers) {
         deps += [ ":chrome_helper_app_${helper_params[0]}" ]
       }
     }
@@ -1187,7 +1262,7 @@
         "//third_party/swiftshader/src/OpenGL/libGLESv2:swiftshader_libGLESv2",
       ]
 
-      foreach(helper_params, content_mac_helpers) {
+      foreach(helper_params, chrome_mac_helpers) {
         _dsyms +=
             [ "$root_out_dir/${chrome_helper_name}${helper_params[2]}.dSYM" ]
         deps += [ ":chrome_helper_app_${helper_params[0]}" ]
diff --git a/chrome/app/helper-alerts-Info.plist b/chrome/app/helper-alerts-Info.plist
new file mode 100644
index 0000000..a1bb91f
--- /dev/null
+++ b/chrome/app/helper-alerts-Info.plist
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleIdentifier</key>
+	<string>${CHROMIUM_BUNDLE_ID}.framework.AlertNotificationService</string>
+	<key>CFBundleIconFile</key>
+	<string>app.icns</string>
+	<key>NSUserNotificationAlertStyle</key>
+	<string>alert</string>
+</dict>
+</plist>
diff --git a/chrome/app/helper-alerts-InfoPlist.strings b/chrome/app/helper-alerts-InfoPlist.strings
new file mode 100644
index 0000000..58af33b
--- /dev/null
+++ b/chrome/app/helper-alerts-InfoPlist.strings
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDisplayName</key>
+	<string>${CHROMIUM_FULL_NAME}</string>
+</dict>
+</plist>
diff --git a/chrome/browser/DEPS b/chrome/browser/DEPS
index 2271be6..a034cf47 100644
--- a/chrome/browser/DEPS
+++ b/chrome/browser/DEPS
@@ -25,6 +25,7 @@
   "+chrome/services/cups_proxy",
   "+chrome/services/diagnosticsd/public",
   "+chrome/services/file_util/public",
+  "+chrome/services/mac_notifications/public",
   "+chrome/services/machine_learning/public",
   "+chrome/services/media_gallery_util/public",
   "+chrome/services/printing/public",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 47899b9..14657b7 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -6233,6 +6233,10 @@
      flag_descriptions::kNewMacNotificationAPIName,
      flag_descriptions::kNewMacNotificationAPIDescription, kOsMac,
      FEATURE_VALUE_TYPE(features::kNewMacNotificationAPI)},
+    {"notifications-via-helper-app",
+     flag_descriptions::kNotificationsViaHelperAppName,
+     flag_descriptions::kNotificationsViaHelperAppDescription, kOsMac,
+     FEATURE_VALUE_TYPE(features::kNotificationsViaHelperApp)},
 #endif
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 7e26112..885b121 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -3646,6 +3646,11 @@
     "expiry_milestone": 94
   },
   {
+    "name": "notifications-via-helper-app",
+    "owners": [ "knollr", "peter" ],
+    "expiry_milestone": 94
+  },
+  {
     "name": "ntp-cache-one-google-bar",
     "owners": [ "aee", "mahmadi", "tiborg" ],
     "expiry_milestone": 92
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 2d362c9..2e3b6f289 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -789,6 +789,11 @@
     "Enables the usage of Apple's new notification API which will run on macOS "
     "10.14+";
 
+const char kNotificationsViaHelperAppName[] = "Notifications via helper app";
+const char kNotificationsViaHelperAppDescription[] =
+    "Enables the notification helper app to display alerts on macOS instead of "
+    "the XPC service";
+
 const char kWinrtGeolocationImplementationName[] =
     "WinRT Geolocation Implementation";
 const char kWinrtGeolocationImplementationDescription[] =
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 124b1e1..def0adb 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -485,6 +485,9 @@
 extern const char kNewMacNotificationAPIName[];
 extern const char kNewMacNotificationAPIDescription[];
 
+extern const char kNotificationsViaHelperAppName[];
+extern const char kNotificationsViaHelperAppDescription[];
+
 extern const char kWinrtGeolocationImplementationName[];
 extern const char kWinrtGeolocationImplementationDescription[];
 
diff --git a/chrome/browser/notifications/mac_notification_provider_factory.mm b/chrome/browser/notifications/mac_notification_provider_factory.mm
index 584c4ce..ab98de5c 100644
--- a/chrome/browser/notifications/mac_notification_provider_factory.mm
+++ b/chrome/browser/notifications/mac_notification_provider_factory.mm
@@ -4,6 +4,8 @@
 
 #include "chrome/browser/notifications/mac_notification_provider_factory.h"
 
+#include "chrome/browser/service_sandbox_type.h"
+#include "chrome/common/child_process_host_flags.h"
 #include "content/public/browser/service_process_host.h"
 #include "content/public/common/content_switches.h"
 
@@ -15,10 +17,9 @@
   return content::ServiceProcessHost::Launch<
       mac_notifications::mojom::MacNotificationProvider>(
       content::ServiceProcessHost::Options()
+          .WithDisplayName("Notification Service")
           .WithExtraCommandLineSwitches({switches::kMessageLoopTypeUi})
-          // TODO(knollr): Set the correct flags so the helper launches via
-          // the app which has set the alert notifications style:
-          //.WithChildFlags(chrome::kChildProcessHelperAlerts)
+          .WithChildFlags(chrome::kChildProcessHelperAlerts)
           .Pass());
 }
 
diff --git a/chrome/browser/service_sandbox_type.h b/chrome/browser/service_sandbox_type.h
index 61b5bb9..e62f111c 100644
--- a/chrome/browser/service_sandbox_type.h
+++ b/chrome/browser/service_sandbox_type.h
@@ -11,6 +11,10 @@
 #include "media/base/media_switches.h"
 #include "sandbox/policy/sandbox_type.h"
 
+#if defined(OS_MAC)
+#include "chrome/services/mac_notifications/public/mojom/mac_notifications.mojom.h"
+#endif  // defined(OS_MAC)
+
 // This file maps service classes to sandbox types.  Services which
 // require a non-utility sandbox can be added here.  See
 // ServiceProcessHost::Launch() for how these templates are consumed.
@@ -184,4 +188,12 @@
 }
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
+#if defined(OS_MAC)
+template <>
+inline sandbox::policy::SandboxType content::GetServiceSandboxType<
+    mac_notifications::mojom::MacNotificationProvider>() {
+  return sandbox::policy::SandboxType::kNoSandbox;
+}
+#endif  // defined(OS_MAC)
+
 #endif  // CHROME_BROWSER_SERVICE_SANDBOX_TYPE_H_
diff --git a/chrome/common/BUILD.gn b/chrome/common/BUILD.gn
index a99755c..39b1950 100644
--- a/chrome/common/BUILD.gn
+++ b/chrome/common/BUILD.gn
@@ -99,6 +99,7 @@
 static_library("common") {
   sources = [
     "all_messages.h",
+    "child_process_host_flags.h",
     "child_process_logging.h",
     "chrome_content_client.cc",
     "chrome_content_client.h",
diff --git a/chrome/common/child_process_host_flags.h b/chrome/common/child_process_host_flags.h
new file mode 100644
index 0000000..8850e0b
--- /dev/null
+++ b/chrome/common/child_process_host_flags.h
@@ -0,0 +1,26 @@
+// Copyright 2021 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_COMMON_CHILD_PROCESS_HOST_FLAGS_H_
+#define CHROME_COMMON_CHILD_PROCESS_HOST_FLAGS_H_
+
+#include "build/build_config.h"
+#include "content/public/common/child_process_host.h"
+
+namespace chrome {
+
+// Flags for Chrome specific child processes to resolve the appropriate process
+// via ChromeContentClient::GetChildProcessPath().
+enum ChildProcessHostFlags {
+#if defined(OS_MAC)
+  // Starts a child process with the macOS alert style to show notifications as
+  // alerts instead of banners which are shown by the main app.
+  kChildProcessHelperAlerts =
+      content::ChildProcessHost::CHILD_EMBEDDER_FIRST + 1,
+#endif  // defined(OS_MAC)
+};
+
+}  // namespace chrome
+
+#endif  // CHROME_COMMON_CHILD_PROCESS_HOST_FLAGS_H_
diff --git a/chrome/common/chrome_content_client.cc b/chrome/common/chrome_content_client.cc
index 4bdfa57..a913b29 100644
--- a/chrome/common/chrome_content_client.cc
+++ b/chrome/common/chrome_content_client.cc
@@ -30,6 +30,7 @@
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
 #include "chrome/common/channel_info.h"
+#include "chrome/common/child_process_host_flags.h"
 #include "chrome/common/child_process_logging.h"
 #include "chrome/common/chrome_constants.h"
 #include "chrome/common/chrome_paths.h"
@@ -596,6 +597,14 @@
 base::FilePath ChromeContentClient::GetChildProcessPath(
     int child_flags,
     const base::FilePath& helpers_path) {
+  std::string helper_name(chrome::kHelperProcessExecutableName);
+  if (child_flags == chrome::kChildProcessHelperAlerts) {
+    helper_name += " (Alerts)";
+    return helpers_path.Append(helper_name + ".app")
+        .Append("Contents")
+        .Append("MacOS")
+        .Append(helper_name);
+  }
   NOTREACHED() << "Unsupported child process flags!";
   return {};
 }
diff --git a/chrome/installer/mac/signing/modification.py b/chrome/installer/mac/signing/modification.py
index 90ded48..3c2b53b 100644
--- a/chrome/installer/mac/signing/modification.py
+++ b/chrome/installer/mac/signing/modification.py
@@ -48,6 +48,30 @@
                                 config.base_config.base_bundle_id,
                                 config.base_bundle_id)
 
+            alert_helper_app_path = os.path.join(
+                paths.work, config.framework_dir, 'Helpers',
+                '{} Helper (Alerts).app'.format(config.product))
+            alert_helper_plist_path = os.path.join(alert_helper_app_path,
+                                                   'Contents', 'Info.plist')
+            with commands.PlistContext(
+                    alert_helper_plist_path,
+                    rewrite=True) as alert_helper_plist:
+                alert_helper_plist[_CF_BUNDLE_ID] = \
+                        alert_helper_plist[_CF_BUNDLE_ID].replace(
+                                config.base_config.base_bundle_id,
+                                config.base_bundle_id)
+
+            alert_helper_plist_strings_path = os.path.join(
+                alert_helper_app_path, 'Contents', 'Resources', 'base.lproj',
+                'InfoPlist.strings')
+            with commands.PlistContext(
+                    alert_helper_plist_strings_path, rewrite=True,
+                    binary=True) as alert_helper_plist_strings:
+                alert_helper_plist_strings[_CF_BUNDLE_DISPLAY_NAME] = \
+                        '{} {}'.format(
+                            alert_helper_plist_strings[_CF_BUNDLE_DISPLAY_NAME],
+                            dist.app_name_fragment)
+
             app_plist[_CF_BUNDLE_DISPLAY_NAME] = '{} {}'.format(
                 app_plist[_CF_BUNDLE_DISPLAY_NAME], dist.app_name_fragment)
             app_plist[_CF_BUNDLE_EXE] = config.app_product
@@ -111,6 +135,14 @@
     commands.copy_files(new_document_icon,
                         os.path.join(resources_dir, 'document.icns'))
 
+    # Also update the icon in the Alert Helper app.
+    alert_helper_resources_dir = os.path.join(
+        paths.work, config.framework_dir, 'Helpers',
+        '{} Helper (Alerts).app'.format(config.product), 'Contents',
+        'Resources')
+    commands.copy_files(new_app_icon,
+                        os.path.join(alert_helper_resources_dir, 'app.icns'))
+
 
 def _rename_enterprise_manifest(paths, dist, config):
     """Modifies and renames the enterprise policy manifest files for channel-
diff --git a/chrome/installer/mac/signing/modification_test.py b/chrome/installer/mac/signing/modification_test.py
index 25e3c64..b802b59 100644
--- a/chrome/installer/mac/signing/modification_test.py
+++ b/chrome/installer/mac/signing/modification_test.py
@@ -21,8 +21,15 @@
         },
         '/$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework/XPCServices/AlertNotificationService.xpc/Contents/Info.plist':
             {
-                'CFBundleIdentifier':
-                    bundle_id + '.AlertNotificationService.xpc'
+                'CFBundleIdentifier': bundle_id + '.AlertNotificationService'
+            },
+        '/$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework/Helpers/Product Helper (Alerts).app/Contents/Info.plist':
+            {
+                'CFBundleIdentifier': bundle_id + '.AlertNotificationService'
+            },
+        '/$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework/Helpers/Product Helper (Alerts).app/Contents/Resources/base.lproj/InfoPlist.strings':
+            {
+                'CFBundleDisplayName': 'Product'
             },
         '/$W/app-entitlements.plist': {
             'com.apple.application-identifier': bundle_id
@@ -383,7 +390,7 @@
             ),
         ])
 
-        self.assertEqual(6, kwargs['copy_files'].call_count)
+        self.assertEqual(7, kwargs['copy_files'].call_count)
         kwargs['copy_files'].assert_has_calls([
             mock.call('/$I/Product Packaging/app-entitlements.plist',
                       '/$W/app-entitlements.plist'),
@@ -398,22 +405,37 @@
                       '/$W/App Product Canary.app/Contents/Resources/app.icns'),
             mock.call(
                 '/$I/Product Packaging/document_canary.icns',
-                '/$W/App Product Canary.app/Contents/Resources/document.icns')
+                '/$W/App Product Canary.app/Contents/Resources/document.icns'),
+            mock.call(
+                '/$I/Product Packaging/app_canary.icns',
+                '/$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework/Helpers/Product Helper (Alerts).app/Contents/Resources/app.icns'
+            )
         ])
         kwargs['write_file'].assert_called_once_with(
             '/$W/App Product Canary.app/Contents/PkgInfo', 'APPLMooo')
 
-        self.assertEqual(7, kwargs['write_plist'].call_count)
+        self.assertEqual(9, kwargs['write_plist'].call_count)
         kwargs['write_plist'].assert_has_calls([
             mock.call(
                 {
                     'CFBundleIdentifier':
-                        'test.signing.bundle_id.canary.AlertNotificationService.xpc'
+                        'test.signing.bundle_id.canary.AlertNotificationService'
                 },
                 '/$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework/XPCServices/AlertNotificationService.xpc/Contents/Info.plist',
                 'xml1'),
             mock.call(
                 {
+                    'CFBundleIdentifier':
+                        'test.signing.bundle_id.canary.AlertNotificationService'
+                },
+                '/$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework/Helpers/Product Helper (Alerts).app/Contents/Info.plist',
+                'xml1'),
+            mock.call({
+                'CFBundleDisplayName': 'Product Canary'
+            }, '/$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework/Helpers/Product Helper (Alerts).app/Contents/Resources/base.lproj/InfoPlist.strings',
+                      'binary1'),
+            mock.call(
+                {
                     'CFBundleDisplayName': 'Product Canary',
                     'CFBundleIdentifier': config.base_bundle_id,
                     'CFBundleExecutable': config.app_product,
diff --git a/chrome/installer/mac/signing/parts.py b/chrome/installer/mac/signing/parts.py
index def2456..9a20b98f 100644
--- a/chrome/installer/mac/signing/parts.py
+++ b/chrome/installer/mac/signing/parts.py
@@ -109,6 +109,14 @@
                 CodeSignOptions.HARDENED_RUNTIME,
                 entitlements='helper-plugin-entitlements.plist',
                 verify_options=verify_options),
+        'helper-alerts':
+            CodeSignedProduct(
+                '{0.framework_dir}/Helpers/{0.product} Helper (Alerts).app'
+                .format(config),
+                '{}.framework.AlertNotificationService'.format(
+                    config.base_bundle_id),
+                options=CodeSignOptions.FULL_HARDENED_RUNTIME_OPTIONS,
+                verify_options=verify_options),
         'app-mode-app':
             CodeSignedProduct(
                 '{.framework_dir}/Helpers/app_mode_loader'.format(config),
diff --git a/chrome/installer/mac/signing/parts_test.py b/chrome/installer/mac/signing/parts_test.py
index 03543172..c2436d80 100644
--- a/chrome/installer/mac/signing/parts_test.py
+++ b/chrome/installer/mac/signing/parts_test.py
@@ -20,6 +20,9 @@
         self.assertEqual(
             'test.signing.bundle_id.framework.AlertNotificationService',
             all_parts['notification-xpc'].identifier)
+        self.assertEqual(
+            'test.signing.bundle_id.framework.AlertNotificationService',
+            all_parts['helper-alerts'].identifier)
         self.assertEqual('test.signing.bundle_id.helper',
                          all_parts['helper-app'].identifier)
 
@@ -33,6 +36,9 @@
         self.assertEqual(
             'test.signing.bundle_id.framework.AlertNotificationService',
             all_parts['notification-xpc'].identifier)
+        self.assertEqual(
+            'test.signing.bundle_id.framework.AlertNotificationService',
+            all_parts['helper-alerts'].identifier)
         self.assertEqual('test.signing.bundle_id.helper',
                          all_parts['helper-app'].identifier)
 
@@ -51,6 +57,9 @@
         self.assertEqual(
             'test.signing.bundle_id.canary.framework.AlertNotificationService',
             all_parts['notification-xpc'].identifier)
+        self.assertEqual(
+            'test.signing.bundle_id.canary.framework.AlertNotificationService',
+            all_parts['helper-alerts'].identifier)
         self.assertEqual('test.signing.bundle_id.helper',
                          all_parts['helper-app'].identifier)
 
@@ -97,6 +106,12 @@
                 model.CodeSignOptions.LIBRARY_VALIDATION +
                 model.CodeSignOptions.KILL +
                 model.CodeSignOptions.HARDENED_RUNTIME),
+            set(all_parts['helper-alerts'].options))
+        self.assertEqual(
+            set(model.CodeSignOptions.RESTRICT +
+                model.CodeSignOptions.LIBRARY_VALIDATION +
+                model.CodeSignOptions.KILL +
+                model.CodeSignOptions.HARDENED_RUNTIME),
             set(all_parts['app-mode-app'].options))
 
 
diff --git a/chrome/installer/mac/signing/pipeline_test.py b/chrome/installer/mac/signing/pipeline_test.py
index ff40f4c..ef19662 100644
--- a/chrome/installer/mac/signing/pipeline_test.py
+++ b/chrome/installer/mac/signing/pipeline_test.py
@@ -264,6 +264,9 @@
             mock.call(
                 '/$W/App Product.app/Contents/Frameworks/Product Framework.framework/Helpers/Product Helper (GPU).app'
             ),
+            mock.call(
+                '/$W/App Product.app/Contents/Frameworks/Product Framework.framework/Helpers/Product Helper (Alerts).app'
+            ),
             mock.call('/$W/App Product.app')
         ])
 
@@ -295,6 +298,9 @@
             mock.call(
                 '/$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework/Helpers/Product Helper (GPU).app'
             ),
+            mock.call(
+                '/$W/App Product Canary.app/Contents/Frameworks/Product Framework.framework/Helpers/Product Helper (Alerts).app'
+            ),
             mock.call('/$W/App Product Canary.app')
         ])
 
diff --git a/tools/mb/mb.py b/tools/mb/mb.py
index 60520e8..64d5d501 100755
--- a/tools/mb/mb.py
+++ b/tools/mb/mb.py
@@ -1408,6 +1408,7 @@
               'ChromiumUpdater.app/',
               'Content Shell.app/',
               'Google Chrome Framework.framework/',
+              'Google Chrome Helper (Alerts).app/',
               'Google Chrome Helper (GPU).app/',
               'Google Chrome Helper (Plugin).app/',
               'Google Chrome Helper (Renderer).app/',
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 53c4035..76ae9e8 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -44802,6 +44802,7 @@
       label="OmniboxDefaultTypedNavigationsToHttps:disabled"/>
   <int value="-850821337" label="WebContentsForceDark:enabled"/>
   <int value="-848691867" label="DesktopPWAWindowing:enabled"/>
+  <int value="-847735582" label="NotificationsViaHelperApp:enabled"/>
   <int value="-847216521" label="ChromeDuplex:enabled"/>
   <int value="-844786349" label="ClipboardHistoryNudgeSessionReset:enabled"/>
   <int value="-844537521" label="HttpFormWarning:disabled"/>
@@ -46515,6 +46516,7 @@
   <int value="745541471" label="PaintHolding:disabled"/>
   <int value="745783589" label="translate-force-trigger-on-english"/>
   <int value="745868416" label="disable-system-timezone-automatic-detection"/>
+  <int value="746294842" label="NotificationsViaHelperApp:disabled"/>
   <int value="746765012" label="AutofillEnableToolbarStatusChip:disabled"/>
   <int value="746944193" label="enable-automatic-password-saving:disabled"/>
   <int value="747076955" label="MobileIdentityConsistency:enabled"/>