chromium/chrome/app_shim/app_shim_controller.mm

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/app_shim/app_shim_controller.h"

#import <Cocoa/Cocoa.h>
#include <mach/message.h>

#include <utility>

#include "base/apple/bundle_locations.h"
#include "base/apple/foundation_util.h"
#include "base/apple/mach_logging.h"
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/hash/md5.h"
#include "base/mac/launch_application.h"
#include "base/mac/mac_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_param_associator.h"
#include "base/metrics/histogram_macros_local.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/synchronization/waitable_event.h"
#import "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/app_shim/app_shim_delegate.h"
#include "chrome/app_shim/app_shim_render_widget_host_view_mac_delegate.h"
#include "chrome/browser/ui/cocoa/browser_window_command_handler.h"
#include "chrome/browser/ui/cocoa/chrome_command_dispatcher_delegate.h"
#include "chrome/browser/ui/cocoa/main_menu_builder.h"
#import "chrome/browser/ui/cocoa/renderer_context_menu/chrome_swizzle_services_menu_updater.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/mac/app_mode_common.h"
#include "chrome/common/process_singleton_lock_posix.h"
#include "chrome/grit/generated_resources.h"
#import "chrome/services/mac_notifications/mac_notification_service_ns.h"
#import "chrome/services/mac_notifications/mac_notification_service_un.h"
#include "components/metrics/child_histogram_fetcher_impl.h"
#include "components/remote_cocoa/app_shim/application_bridge.h"
#include "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#include "components/remote_cocoa/common/application.mojom.h"
#include "components/variations/field_trial_config/field_trial_util.h"
#include "components/variations/variations_switches.h"
#include "content/public/browser/remote_cocoa.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/platform/named_platform_channel.h"
#include "mojo/public/cpp/platform/platform_channel.h"
#include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/screen.h"
#include "ui/gfx/image/image.h"

// The ProfileMenuTarget bridges between Objective C (as the target for the
// profile menu NSMenuItems) and C++ (the mojo methods called by
// AppShimController).
@interface ProfileMenuTarget : NSObject {
  raw_ptr<AppShimController> _controller;
}
- (instancetype)initWithController:(AppShimController*)controller;
- (void)clearController;
@end

@implementation ProfileMenuTarget
- (instancetype)initWithController:(AppShimController*)controller {
  if (self = [super init])
    _controller = controller;
  return self;
}

- (void)clearController {
  _controller = nullptr;
}

- (void)profileMenuItemSelected:(id)sender {
  if (_controller)
    _controller->ProfileMenuItemSelected([sender tag]);
}

- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
  return YES;
}
@end

// The ApplicationDockMenuTarget bridges between Objective C (as the target for
// the profile menu NSMenuItems) and C++ (the mojo methods called by
// AppShimController).
@interface ApplicationDockMenuTarget : NSObject {
  raw_ptr<AppShimController> _controller;
}
- (instancetype)initWithController:(AppShimController*)controller;
- (void)clearController;
@end

@implementation ApplicationDockMenuTarget
- (instancetype)initWithController:(AppShimController*)controller {
  if (self = [super init])
    _controller = controller;
  return self;
}

- (void)clearController {
  _controller = nullptr;
}

- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
  return YES;
}

- (void)commandFromDock:(id)sender {
  if (_controller)
    _controller->CommandFromDock([sender tag]);
}

@end

namespace {
// The maximum amount of time to wait for Chrome's AppShimListener to be
// ready.
constexpr base::TimeDelta kPollTimeoutSeconds = base::Seconds(60);

// The period in between attempts to check of Chrome's AppShimListener is
// ready.
constexpr base::TimeDelta kPollPeriodMsec = base::Milliseconds(100);

// Helper that keeps stops another sequence from executing any code while this
// object is alive. The constructor waits for the other thread to be blocked,
// while the destructor signals the other thread to continue running again.
class ScopedSynchronizeThreads {
 public:
  explicit ScopedSynchronizeThreads(
      scoped_refptr<base::SequencedTaskRunner> thread_runner) {
    // This event is signalled by a task posted to the other thread as soon as
    // it starts executing, to signal that no more code is running on that
    // thread. The main thread only proceeds after this event is signalled.
    base::WaitableEvent thread_blocked;
    // Heap allocate `operation_finished` and make sure it is destroyed on the
    // thread that waits on that event. This ensures it isn't destroyed too
    // early.
    auto operation_finished = std::make_unique<base::WaitableEvent>();
    operation_finished_ = operation_finished.get();
    thread_runner->PostTask(
        FROM_HERE,
        base::BindOnce(
            [](base::WaitableEvent* thread_blocked,
               std::unique_ptr<base::WaitableEvent> operation_finished) {
              thread_blocked->Signal();
              operation_finished->Wait();
            },
            &thread_blocked, std::move(operation_finished)));
    thread_blocked.Wait();
  }

  ~ScopedSynchronizeThreads() { operation_finished_->Signal(); }

 private:
  // This event is signalled by the main thread to indicate that all the work is
  // done, allowing the other thread to be unblocked again.
  raw_ptr<base::WaitableEvent> operation_finished_;
};

}  // namespace

AppShimController::Params::Params() = default;
AppShimController::Params::Params(const Params& other) = default;
AppShimController::Params::~Params() = default;

AppShimController::AppShimController(const Params& params)
    : params_(params),
      host_receiver_(host_.BindNewPipeAndPassReceiver()),
      delegate_([[AppShimDelegate alloc] initWithController:this]),
      profile_menu_target_([[ProfileMenuTarget alloc] initWithController:this]),
      application_dock_menu_target_(
          [[ApplicationDockMenuTarget alloc] initWithController:this]) {
  screen_ = std::make_unique<display::ScopedNativeScreen>();
  NSApp.delegate = delegate_;

  [ChromeSwizzleServicesMenuUpdater install];

  // Since this is early startup code, there is no guarantee that the state of
  // the features being tested for here matches the state from the eventualy
  // Chrome we connect to (although they will match the vast majority of the
  // time). Creating the notification service when it ends up being not needed
  // is harmless, and the only effect of not creating it early when we later
  // need it is that we might miss some notification actions, which again is
  // harmless.
  if (base::FeatureList::IsEnabled(features::kAppShimNotificationAttribution) &&
      WebAppIsAdHocSigned()) {
    // `notification_service_` needs to be created early during start up to make
    // sure it is able to install its delegate before the OS attempts to inform
    // it of any notification actions that might have happened.
    notification_service_ =
        std::make_unique<mac_notifications::MacNotificationServiceUN>(
            std::move(notification_action_handler_remote_),
            base::BindRepeating(
                &AppShimController::NotificationPermissionStatusChanged,
                base::Unretained(this)),
            UNUserNotificationCenter.currentNotificationCenter);
  }
}

AppShimController::~AppShimController() {
  // Un-set the delegate since NSApplication does not retain it.
  NSApp.delegate = nil;
  [profile_menu_target_ clearController];
  [application_dock_menu_target_ clearController];
}

// static
void AppShimController::PreInitFeatureState(
    const base::CommandLine& command_line) {
  new base::FieldTrialList();

  auto feature_list = std::make_unique<base::FeatureList>();
  base::FeatureList::FailOnFeatureAccessWithoutFeatureList();

  // App shims can generally be launched in one of two ways:
  // - By chrome itself, in which case the full feature and field trial state
  //   is passed on the command line, and any state stored in user_data_dir is
  //   ignored. In this case we could avoid re-initializing feature and field
  //   trial state in FinalizeFeatureState entirely, but since this is the case
  //   with by far the most test coverage, doing so would make it much more
  //   likely that some change accidentally slips in that would breaks with the
  //   early access and reinitialization behavior.
  // - By the OS or user directly. In which case a (possibly outdated) state is
  //   loaded from user_data_dir, but we do allow explicit feature and/or field
  //   trial overrides on the command line to help with manual
  //   testing/development.
  //
  // In both cases the state initialized here is only used during startup, and
  // as soon as a mojo connection has been established with Chrome the final
  // feature state is passed to FinalizeFeatureState below.
  //
  // Several integration tests launch app shims with somewhat of a mix of these
  // two options. Where tests try to simulate an app shim being launched by the
  // OS we still pass switches such as the kLaunchedByChromeProcessId (to ensure
  // the app shim communicates with the correct test instance), but don't pass
  // the full feature state on the command line, so having
  // kLaunchedByChromeProcessId be present does not guarantee that feature state
  // is passed on the command line as well.

  // Add command line overrides. These will always be set if this app shim is
  // launched by chrome, but for development/testing purposes can also be used
  // to override state found in the user_data_dir file if the launch was not
  // triggered by chrome. In either case, FinalizeFeatureState will reset all
  // feature and field trial state to match the state of the running Chrome
  // instance.
  variations::VariationsCommandLine::GetForCommandLine(command_line)
      .ApplyToFeatureAndFieldTrialList(feature_list.get());

  // If the shim was launched by chrome, we're done. However if the shim was
  // launched directly by the user/OS the command line parameters were merely
  // optional overrides, so read state from the file in user_data_dir to get the
  // correct feature and field trial state for features and field trials that
  // have not already been explicitly overridden.
  if (!command_line.HasSwitch(app_mode::kLaunchedByChromeProcessId)) {
    auto file_state = variations::VariationsCommandLine::ReadFromFile(
        base::PathService::CheckedGet(chrome::DIR_USER_DATA)
            .Append(app_mode::kFeatureStateFileName));
    if (file_state.has_value()) {
      file_state->ApplyToFeatureAndFieldTrialList(feature_list.get());
    }
  }

  // Until FinalizeFeatureState() is called, only features whose name is in the
  // below list are allowed to be passed to base::FeatureList::IsEnabled().
  // Attempts to check the state of any other feature will behave as if no
  // FeatureList was set yet at all (i.e. check-fail).
  base::FeatureList::SetEarlyAccessInstance(
      std::move(feature_list),
      {"AppShimLaunchChromeSilently", "AppShimNotificationAttribution",
       "DcheckIsFatal", "MojoBindingsInlineSLS", "MojoInlineMessagePayloads",
       "MojoIpcz", "MojoIpczMemV2", "MojoTaskPerMessage",
       "StandardCompliantHostCharacters",
       "StandardCompliantNonSpecialSchemeURLParsing",
       "UseAdHocSigningForWebAppShims", "UseIDNA2008NonTransitional",
       "SonomaAccessibilityActivationRefinements"});
}

// static
void AppShimController::FinalizeFeatureState(
    const variations::VariationsCommandLine& feature_state,
    const scoped_refptr<base::SequencedTaskRunner>& io_thread_runner) {
  // This code assumes no other threads are running. So make sure there is no
  // started ThreadPoolInstance, and block the IO thread for the duration of
  // this method.
  CHECK(!base::ThreadPoolInstance::Get() ||
        !base::ThreadPoolInstance::Get()->WasStarted());
  ScopedSynchronizeThreads block_io_thread(io_thread_runner);

  // Recreate FieldTrialList.
  std::unique_ptr<base::FieldTrialList> old_field_trial_list(
      base::FieldTrialList::ResetInstance());
  CHECK(old_field_trial_list);
  // This is intentionally leaked since it needs to live for the duration of
  // the app shim process and there's no benefit in cleaning it up at exit.
  auto* field_trial_list = new base::FieldTrialList();
  ANNOTATE_LEAKING_OBJECT_PTR(field_trial_list);
  std::ignore = field_trial_list;

  // Reset FieldTrial parameter cache.
  base::FieldTrialParamAssociator::GetInstance()->ClearAllCachedParams({});

  // Create a new FeatureList and field trial state using what was passed by the
  // browser process.
  auto feature_list = std::make_unique<base::FeatureList>();
  feature_state.ApplyToFeatureAndFieldTrialList(feature_list.get());

  base::FeatureList::SetInstance(std::move(feature_list));
}

void AppShimController::OnAppFinishedLaunching(
    bool launched_by_notification_action) {
  DCHECK_EQ(init_state_, InitState::kWaitingForAppToFinishLaunch);
  init_state_ = InitState::kWaitingForChromeReady;
  launched_by_notification_action_ = launched_by_notification_action;

  if (FindOrLaunchChrome()) {
    // Start polling to see if Chrome is ready to connect.
    PollForChromeReady(kPollTimeoutSeconds);
  }

  // Otherwise, Chrome is in the process of launching and `PollForChromeReady`
  // will be called when launching is complete.
}

bool AppShimController::FindOrLaunchChrome() {
  DCHECK(!chrome_to_connect_to_);
  DCHECK(!chrome_launched_by_app_);
  const base::CommandLine* app_command_line =
      base::CommandLine::ForCurrentProcess();

  // If this shim was launched by Chrome, only connect to that that specific
  // process.
  if (app_command_line->HasSwitch(app_mode::kLaunchedByChromeProcessId)) {
    std::string chrome_pid_string = app_command_line->GetSwitchValueASCII(
        app_mode::kLaunchedByChromeProcessId);

    int chrome_pid;
    if (!base::StringToInt(chrome_pid_string, &chrome_pid)) {
      LOG(FATAL) << "Invalid PID: " << chrome_pid_string;
    }

    chrome_to_connect_to_ = [NSRunningApplication
        runningApplicationWithProcessIdentifier:chrome_pid];
    if (!chrome_to_connect_to_) {
      // Sometimes runningApplicationWithProcessIdentifier fails to return the
      // application, even though it exists. If that happens, try to find the
      // running application in the full list of running applications manually.
      // See https://crbug.com/1426897.
      NSArray<NSRunningApplication*>* apps =
          NSWorkspace.sharedWorkspace.runningApplications;
      for (unsigned i = 0; i < apps.count; ++i) {
        if (apps[i].processIdentifier == chrome_pid) {
          chrome_to_connect_to_ = apps[i];
        }
      }
      if (!chrome_to_connect_to_) {
        LOG(FATAL) << "Failed to open process with PID: " << chrome_pid;
      }
    }

    return true;
  }

  // Query the singleton lock. If the lock exists and specifies a running
  // Chrome, then connect to that process. Otherwise, launch a new Chrome
  // process.
  chrome_to_connect_to_ = FindChromeFromSingletonLock(params_.user_data_dir);
  if (chrome_to_connect_to_) {
    return true;
  }

  // In tests, launching Chrome does nothing.
  if (app_command_line->HasSwitch(app_mode::kLaunchedForTest)) {
    return true;
  }

  // Otherwise, launch Chrome.
  base::FilePath chrome_bundle_path = base::apple::OuterBundlePath();
  LOG(INFO) << "Launching " << chrome_bundle_path.value();
  base::CommandLine browser_command_line(base::CommandLine::NO_PROGRAM);
  browser_command_line.AppendSwitchPath(switches::kUserDataDir,
                                        params_.user_data_dir);

  // Forward feature and field trial related switches to Chrome to aid in
  // testing and development with custom feature or field trial configurations.
  static constexpr const char* switches_to_forward[] = {
      switches::kEnableFeatures, switches::kDisableFeatures,
      switches::kForceFieldTrials,
      variations::switches::kForceFieldTrialParams};
  for (const char* switch_name : switches_to_forward) {
    if (app_command_line->HasSwitch(switch_name)) {
      browser_command_line.AppendSwitchASCII(
          switch_name, app_command_line->GetSwitchValueASCII(switch_name));
    }
  }

  const bool silent_chrome_launch =
      base::FeatureList::IsEnabled(features::kAppShimLaunchChromeSilently);
  if (silent_chrome_launch) {
    browser_command_line.AppendSwitch(switches::kNoStartupWindow);
  }

  base::mac::LaunchApplication(
      chrome_bundle_path, browser_command_line, /*url_specs=*/{},
      {.create_new_instance = true,
       .hidden_in_background = silent_chrome_launch},
      base::BindOnce(
          [](AppShimController* shim_controller, NSRunningApplication* app,
             NSError* error) {
            if (error) {
              LOG(FATAL) << "Failed to launch Chrome.";
            }

            shim_controller->chrome_launched_by_app_ = app;

            // Start polling to see if Chrome is ready to connect.
            shim_controller->PollForChromeReady(kPollTimeoutSeconds);
          },
          // base::Unretained is safe because this is a singleton.
          base::Unretained(this)));

  return false;
}

// static
NSRunningApplication* AppShimController::FindChromeFromSingletonLock(
    const base::FilePath& user_data_dir) {
  base::FilePath lock_symlink_path =
      user_data_dir.Append(chrome::kSingletonLockFilename);
  std::string hostname;
  int pid = -1;
  if (!ParseProcessSingletonLock(lock_symlink_path, &hostname, &pid)) {
    // This indicates that there is no Chrome process running (or that has been
    // running long enough to get the lock).
    LOG(INFO) << "Singleton lock not found at " << lock_symlink_path.value();
    return nil;
  }

  // Open the associated pid. This could be invalid if Chrome terminated
  // abnormally and didn't clean up.
  NSRunningApplication* process_from_lock =
      [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
  if (!process_from_lock) {
    LOG(WARNING) << "Singleton lock pid " << pid << " invalid.";
    return nil;
  }

  // Check the process' bundle id. As above, the specified pid could have been
  // reused by some other process.
  NSString* expected_bundle_id = base::apple::OuterBundle().bundleIdentifier;
  NSString* lock_bundle_id = process_from_lock.bundleIdentifier;
  if (![expected_bundle_id isEqualToString:lock_bundle_id]) {
    LOG(WARNING) << "Singleton lock pid " << pid
                 << " has unexpected bundle id.";
    return nil;
  }

  return process_from_lock;
}

void AppShimController::PollForChromeReady(
    const base::TimeDelta& time_until_timeout) {
  // If the Chrome process we planned to connect to is not running anymore,
  // quit.
  if (chrome_to_connect_to_ && chrome_to_connect_to_.terminated) {
    LOG(FATAL) << "Running chrome instance terminated before connecting.";
  }

  // If we launched a Chrome process and it has terminated, then that most
  // likely means that it did not get the singleton lock (which means that we
  // should find the processes that did below).
  bool launched_chrome_is_terminated =
      chrome_launched_by_app_ && chrome_launched_by_app_.terminated;

  // If we haven't found the Chrome process that got the singleton lock, check
  // now.
  if (!chrome_to_connect_to_)
    chrome_to_connect_to_ = FindChromeFromSingletonLock(params_.user_data_dir);

  // If our launched Chrome has terminated, then there should have existed a
  // process holding the singleton lock.
  if (launched_chrome_is_terminated && !chrome_to_connect_to_)
    LOG(FATAL) << "Launched Chrome has exited and singleton lock not taken.";

  // Poll to see if the mojo channel is ready. Of note is that we don't actually
  // verify that |endpoint| is connected to |chrome_to_connect_to_|.
  {
    mojo::PlatformChannelEndpoint endpoint;
    NSString* browser_bundle_id =
        base::apple::ObjCCast<NSString>([NSBundle.mainBundle
            objectForInfoDictionaryKey:app_mode::kBrowserBundleIDKey]);
    CHECK(browser_bundle_id);
    const std::string server_name = base::StringPrintf(
        "%s.%s.%s", base::SysNSStringToUTF8(browser_bundle_id).c_str(),
        app_mode::kAppShimBootstrapNameFragment,
        base::MD5String(params_.user_data_dir.value()).c_str());
    endpoint = ConnectToBrowser(server_name);
    if (endpoint.is_valid()) {
      LOG(INFO) << "Connected to " << server_name;
      SendBootstrapOnShimConnected(std::move(endpoint));
      return;
    }
  }

  // Otherwise, try again after a brief delay.
  if (time_until_timeout < kPollPeriodMsec)
    LOG(FATAL) << "Timed out waiting for running chrome instance to be ready.";
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&AppShimController::PollForChromeReady,
                     base::Unretained(this),
                     time_until_timeout - kPollPeriodMsec),
      kPollPeriodMsec);
}

// static
mojo::PlatformChannelEndpoint AppShimController::ConnectToBrowser(
    const mojo::NamedPlatformChannel::ServerName& server_name) {
  // Normally NamedPlatformChannel is used for point-to-point peer
  // communication. For apps shims, the same server is used to establish
  // connections between multiple shim clients and the server. To do this,
  // the shim creates a local PlatformChannel and sends the local (send)
  // endpoint to the server in a raw Mach message. The server uses that to
  // establish an IsolatedConnection, which the client does as well with the
  // remote (receive) end.
  mojo::PlatformChannelEndpoint server_endpoint =
      mojo::NamedPlatformChannel::ConnectToServer(server_name);
  // The browser may still be in the process of launching, so the endpoint
  // may not yet be available.
  if (!server_endpoint.is_valid())
    return mojo::PlatformChannelEndpoint();

  mojo::PlatformChannel channel;
  mach_msg_base_t message{};
  message.header.msgh_id = app_mode::kBootstrapMsgId;
  message.header.msgh_bits =
      MACH_MSGH_BITS(MACH_MSG_TYPE_MOVE_SEND, MACH_MSG_TYPE_MOVE_SEND);
  message.header.msgh_size = sizeof(message);
  message.header.msgh_local_port =
      channel.TakeLocalEndpoint().TakePlatformHandle().ReleaseMachSendRight();
  message.header.msgh_remote_port =
      server_endpoint.TakePlatformHandle().ReleaseMachSendRight();
  kern_return_t kr = mach_msg_send(&message.header);
  if (kr != KERN_SUCCESS) {
    MACH_LOG(ERROR, kr) << "mach_msg_send";
    return mojo::PlatformChannelEndpoint();
  }
  return channel.TakeRemoteEndpoint();
}

void AppShimController::SendBootstrapOnShimConnected(
    mojo::PlatformChannelEndpoint endpoint) {
  DCHECK_EQ(init_state_, InitState::kWaitingForChromeReady);
  init_state_ = InitState::kHasSentOnShimConnected;

  // Chrome will relaunch shims when relaunching apps.
  [NSApp disableRelaunchOnLogin];
  CHECK(!params_.user_data_dir.empty());

  mojo::ScopedMessagePipeHandle message_pipe =
      bootstrap_mojo_connection_.Connect(std::move(endpoint));
  CHECK(message_pipe.is_valid());
  host_bootstrap_.Bind(mojo::PendingRemote<chrome::mojom::AppShimHostBootstrap>(
      std::move(message_pipe), 0));
  host_bootstrap_.set_disconnect_with_reason_handler(base::BindOnce(
      &AppShimController::BootstrapChannelError, base::Unretained(this)));

  auto app_shim_info = chrome::mojom::AppShimInfo::New();
  app_shim_info->profile_path = params_.profile_dir;
  app_shim_info->app_id = params_.app_id;
  app_shim_info->app_url = params_.app_url;
  // If the app shim was launched for a notification action, we don't want to
  // automatically launch the app as well. So do a kRegisterOnly launch
  // instead.
  app_shim_info->launch_type =
      launched_by_notification_action_
          ? chrome::mojom::AppShimLaunchType::kNotificationAction
      : (base::CommandLine::ForCurrentProcess()->HasSwitch(
             app_mode::kLaunchedByChromeProcessId) &&
         !base::CommandLine::ForCurrentProcess()->HasSwitch(
             app_mode::kIsNormalLaunch))
          ? chrome::mojom::AppShimLaunchType::kRegisterOnly
          : chrome::mojom::AppShimLaunchType::kNormal;
  app_shim_info->files = launch_files_;
  app_shim_info->urls = launch_urls_;

  if (base::mac::WasLaunchedAsHiddenLoginItem()) {
    app_shim_info->login_item_restore_state =
        chrome::mojom::AppShimLoginItemRestoreState::kHidden;
  } else if (base::mac::WasLaunchedAsLoginOrResumeItem()) {
    app_shim_info->login_item_restore_state =
        chrome::mojom::AppShimLoginItemRestoreState::kWindowed;
  } else {
    app_shim_info->login_item_restore_state =
        chrome::mojom::AppShimLoginItemRestoreState::kNone;
  }

  app_shim_info->notification_action_handler =
      std::move(notification_action_handler_receiver_);

  host_bootstrap_->OnShimConnected(
      std::move(host_receiver_), std::move(app_shim_info),
      base::BindOnce(&AppShimController::OnShimConnectedResponse,
                     base::Unretained(this)));
  LOG(INFO) << "Sent OnShimConnected";
}

void AppShimController::SetUpMenu() {
  chrome::BuildMainMenu(NSApp, delegate_, params_.app_name, true);
  UpdateProfileMenu(std::vector<chrome::mojom::ProfileMenuItemPtr>());
}

void AppShimController::BootstrapChannelError(uint32_t custom_reason,
                                              const std::string& description) {
  // The bootstrap channel is expected to close after the response to
  // OnShimConnected is received.
  if (init_state_ == InitState::kHasReceivedOnShimConnectedResponse)
    return;
  LOG(ERROR) << "Bootstrap Channel error custom_reason:" << custom_reason
             << " description: " << description;
  [NSApp terminate:nil];
}

void AppShimController::ChannelError(uint32_t custom_reason,
                                     const std::string& description) {
  LOG(ERROR) << "Channel error custom_reason:" << custom_reason
             << " description: " << description;
  [NSApp terminate:nil];
}

void AppShimController::OnShimConnectedResponse(
    chrome::mojom::AppShimLaunchResult result,
    variations::VariationsCommandLine feature_state,
    mojo::PendingReceiver<chrome::mojom::AppShim> app_shim_receiver) {
  LOG(INFO) << "Received OnShimConnected.";
  DCHECK_EQ(init_state_, InitState::kHasSentOnShimConnected);
  init_state_ = InitState::kHasReceivedOnShimConnectedResponse;

  // Finalize feature state and finish up initialization that was deferred for
  // feature state to be fully setup.
  FinalizeFeatureState(feature_state, params_.io_thread_runner);
  base::ThreadPoolInstance::Get()->StartWithDefaultParams();
  SetUpMenu();

  if (result != chrome::mojom::AppShimLaunchResult::kSuccess) {
    switch (result) {
      case chrome::mojom::AppShimLaunchResult::kSuccess:
        break;
      case chrome::mojom::AppShimLaunchResult::kSuccessAndDisconnect:
        LOG(ERROR) << "Launched successfully, but do not maintain connection.";
        break;
      case chrome::mojom::AppShimLaunchResult::kDuplicateHost:
        LOG(ERROR) << "An AppShimHostBootstrap already exists for this app.";
        break;
      case chrome::mojom::AppShimLaunchResult::kProfileNotFound:
        LOG(ERROR) << "No suitable profile found.";
        break;
      case chrome::mojom::AppShimLaunchResult::kAppNotFound:
        LOG(ERROR) << "App not installed for specified profile.";
        break;
      case chrome::mojom::AppShimLaunchResult::kProfileLocked:
        LOG(ERROR) << "Profile locked.";
        break;
      case chrome::mojom::AppShimLaunchResult::kFailedValidation:
        LOG(ERROR) << "Validation failed.";
        break;
    };
    [NSApp terminate:nil];
    return;
  }
  shim_receiver_.Bind(std::move(app_shim_receiver),
                      ui::WindowResizeHelperMac::Get()->task_runner());
  shim_receiver_.set_disconnect_with_reason_handler(
      base::BindOnce(&AppShimController::ChannelError, base::Unretained(this)));

  host_bootstrap_.reset();
}

void AppShimController::CreateRemoteCocoaApplication(
    mojo::PendingAssociatedReceiver<remote_cocoa::mojom::Application>
        receiver) {
  remote_cocoa::ApplicationBridge::Get()->BindReceiver(std::move(receiver));
  remote_cocoa::ApplicationBridge::Get()->SetContentNSViewCreateCallbacks(
      base::BindRepeating(&AppShimController::CreateRenderWidgetHostNSView),
      base::BindRepeating(remote_cocoa::CreateWebContentsNSView));
}

void AppShimController::CreateRenderWidgetHostNSView(
    uint64_t view_id,
    mojo::ScopedInterfaceEndpointHandle host_handle,
    mojo::ScopedInterfaceEndpointHandle view_request_handle) {
  remote_cocoa::RenderWidgetHostViewMacDelegateCallback
      responder_delegate_creation_callback =
          base::BindOnce(&AppShimController::GetDelegateForHost, view_id);
  remote_cocoa::CreateRenderWidgetHostNSView(
      view_id, std::move(host_handle), std::move(view_request_handle),
      std::move(responder_delegate_creation_callback));
}

NSObject<RenderWidgetHostViewMacDelegate>*
AppShimController::GetDelegateForHost(uint64_t view_id) {
  return [[AppShimRenderWidgetHostViewMacDelegate alloc]
      initWithRenderWidgetHostNSViewID:view_id];
}

void AppShimController::CreateCommandDispatcherForWidget(uint64_t widget_id) {
  if (auto* bridge =
          remote_cocoa::NativeWidgetNSWindowBridge::GetFromId(widget_id)) {
    bridge->SetCommandDispatcher([[ChromeCommandDispatcherDelegate alloc] init],
                                 [[BrowserWindowCommandHandler alloc] init]);
  } else {
    LOG(ERROR) << "Failed to find host for command dispatcher.";
  }
}

void AppShimController::SetBadgeLabel(const std::string& badge_label) {
  NSApp.dockTile.badgeLabel = base::SysUTF8ToNSString(badge_label);
}

void AppShimController::UpdateProfileMenu(
    std::vector<chrome::mojom::ProfileMenuItemPtr> profile_menu_items) {
  profile_menu_items_ = std::move(profile_menu_items);

  NSMenuItem* cocoa_profile_menu =
      [NSApp.mainMenu itemWithTag:IDC_PROFILE_MAIN_MENU];
  if (profile_menu_items_.empty()) {
    cocoa_profile_menu.submenu = nil;
    cocoa_profile_menu.hidden = YES;
    return;
  }
  cocoa_profile_menu.hidden = NO;

  NSMenu* menu = [[NSMenu alloc]
      initWithTitle:l10n_util::GetNSStringWithFixup(IDS_PROFILES_MENU_NAME)];
  [cocoa_profile_menu setSubmenu:menu];

  // Note that this code to create menu items is nearly identical to the code
  // in ProfileMenuController in the browser process.
  for (size_t i = 0; i < profile_menu_items_.size(); ++i) {
    const auto& mojo_item = profile_menu_items_[i];
    NSString* name = base::SysUTF16ToNSString(mojo_item->name);
    NSMenuItem* item =
        [[NSMenuItem alloc] initWithTitle:name
                                   action:@selector(profileMenuItemSelected:)
                            keyEquivalent:@""];
    item.tag = mojo_item->menu_index;
    item.state =
        mojo_item->active ? NSControlStateValueOn : NSControlStateValueOff;
    item.target = profile_menu_target_;
    gfx::Image icon(mojo_item->icon);
    item.image = icon.AsNSImage();
    [menu insertItem:item atIndex:i];
  }
}

void AppShimController::UpdateApplicationDockMenu(
    std::vector<chrome::mojom::ApplicationDockMenuItemPtr> dock_menu_items) {
  dock_menu_items_ = std::move(dock_menu_items);
}

void AppShimController::BindNotificationProvider(
    mojo::PendingReceiver<mac_notifications::mojom::MacNotificationProvider>
        provider) {
  notifications_receiver_.reset();
  notifications_receiver_.Bind(std::move(provider));
}

void AppShimController::RequestNotificationPermission(
    RequestNotificationPermissionCallback callback) {
  if (!notification_service_un()) {
    std::move(callback).Run(
        mac_notifications::mojom::RequestPermissionResult::kRequestFailed);
    return;
  }
  notification_service_un()->RequestPermission(std::move(callback));
}

void AppShimController::BindNotificationService(
    mojo::PendingReceiver<mac_notifications::mojom::MacNotificationService>
        service,
    mojo::PendingRemote<mac_notifications::mojom::MacNotificationActionHandler>
        handler) {
  CHECK(
      base::FeatureList::IsEnabled(features::kAppShimNotificationAttribution));
  // TODO(crbug.com/40616749): Once ad-hoc signed app shims become the
  // default on supported platforms, change this to always use the
  // UNUserNotification API (and not support notification attribution on other
  // platforms at all).
  if (WebAppIsAdHocSigned()) {
    // While the constructor should have created the `notification_service_`
    // instance already, it is possible that the base::FeatureList state at the
    // time did not match the current Chrome state, so make sure to create the
    // service now if it wasn't created already.
    if (!notification_service_) {
      CHECK(notification_action_handler_remote_);
      notification_service_ =
          std::make_unique<mac_notifications::MacNotificationServiceUN>(
              std::move(notification_action_handler_remote_),
              base::BindRepeating(
                  &AppShimController::NotificationPermissionStatusChanged,
                  base::Unretained(this)),
              UNUserNotificationCenter.currentNotificationCenter);
    }
    // Note that `handler` as passed in to this method is ignored. Notification
    // actions instead will be dispatched to the app-shim scoped mojo pipe that
    // was established earlier during startup, to allow notification actions to
    // be triggered before the browser process tries to connect to the
    // notification service.
    notification_service_un()->Bind(std::move(service));
    // TODO(crbug.com/40616749): Determine when to ask for permissions.
    notification_service_un()->RequestPermission(base::DoNothing());
  } else {
    // NSUserNotificationCenter is in the process of being replaced, and
    // warnings about its deprecation are not helpful. https://crbug.com/1127306
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    notification_service_ =
        std::make_unique<mac_notifications::MacNotificationServiceNS>(
            std::move(service), std::move(handler),
            [NSUserNotificationCenter defaultUserNotificationCenter]);
#pragma clang diagnostic pop
  }
}

mac_notifications::MacNotificationServiceUN*
AppShimController::notification_service_un() {
  if (!WebAppIsAdHocSigned()) {
    return nullptr;
  }
  return static_cast<mac_notifications::MacNotificationServiceUN*>(
      notification_service_.get());
}

void AppShimController::NotificationPermissionStatusChanged(
    mac_notifications::mojom::PermissionStatus status) {
  host_->NotificationPermissionStatusChanged(status);
}

void AppShimController::SetUserAttention(
    chrome::mojom::AppShimAttentionType attention_type) {
  switch (attention_type) {
    case chrome::mojom::AppShimAttentionType::kCancel:
      [NSApp cancelUserAttentionRequest:attention_request_id_];
      attention_request_id_ = 0;
      break;
    case chrome::mojom::AppShimAttentionType::kCritical:
      attention_request_id_ = [NSApp requestUserAttention:NSCriticalRequest];
      break;
  }
}

void AppShimController::OpenFiles(const std::vector<base::FilePath>& files) {
  if (init_state_ == InitState::kWaitingForAppToFinishLaunch) {
    launch_files_ = files;
  } else {
    host_->FilesOpened(files);
  }
}

void AppShimController::ProfileMenuItemSelected(uint32_t index) {
  for (const auto& mojo_item : profile_menu_items_) {
    if (mojo_item->menu_index == index) {
      host_->ProfileSelectedFromMenu(mojo_item->profile_path);
      return;
    }
  }
}

void AppShimController::OpenUrls(const std::vector<GURL>& urls) {
  if (init_state_ == InitState::kWaitingForAppToFinishLaunch) {
    launch_urls_ = urls;
  } else {
    host_->UrlsOpened(urls);
  }
}

void AppShimController::CommandFromDock(uint32_t index) {
  DCHECK(0 <= index && index < dock_menu_items_.size());
  DCHECK(init_state_ != InitState::kWaitingForAppToFinishLaunch);

  [NSApp activateIgnoringOtherApps:YES];
  host_->OpenAppWithOverrideUrl(dock_menu_items_[index]->url);
}

void AppShimController::CommandDispatch(int command_id) {
  switch (command_id) {
    case IDC_WEB_APP_SETTINGS:
      host_->OpenAppSettings();
      break;
    case IDC_NEW_WINDOW:
      host_->ReopenApp();
      break;
  }
}

NSMenu* AppShimController::GetApplicationDockMenu() {
  if (init_state_ == InitState::kWaitingForAppToFinishLaunch ||
      dock_menu_items_.size() == 0)
    return nullptr;

  NSMenu* dockMenu = [[NSMenu alloc] initWithTitle:@""];

  for (size_t i = 0; i < dock_menu_items_.size(); ++i) {
    const auto& mojo_item = dock_menu_items_[i];
    NSString* name = base::SysUTF16ToNSString(mojo_item->name);
    NSMenuItem* item =
        [[NSMenuItem alloc] initWithTitle:name
                                   action:@selector(commandFromDock:)
                            keyEquivalent:@""];
    item.tag = i;
    item.target = application_dock_menu_target_;
    item.enabled =
        [application_dock_menu_target_ validateUserInterfaceItem:item];
    [dockMenu addItem:item];
  }

  return dockMenu;
}

void AppShimController::ApplicationWillTerminate() {
  // Local histogram to let tests verify that histograms are emitted properly.
  LOCAL_HISTOGRAM_BOOLEAN("AppShim.WillTerminate", true);
  host_->ApplicationWillTerminate();
}

void AppShimController::BindChildHistogramFetcherFactory(
    mojo::PendingReceiver<metrics::mojom::ChildHistogramFetcherFactory>
        receiver) {
  metrics::ChildHistogramFetcherFactoryImpl::Create(std::move(receiver));
}

bool AppShimController::WebAppIsAdHocSigned() const {
  NSNumber* isAdHocSigned =
      NSBundle.mainBundle.infoDictionary[app_mode::kCrAppModeIsAdHocSignedKey];
  return isAdHocSigned.boolValue;
}