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

#import "chrome/browser/app_controller_mac.h"

#include <AppKit/AppKit.h>
#include <dispatch/dispatch.h>
#include <stddef.h>

#include <algorithm>
#include <memory>
#include <vector>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/auto_reset.h"
#include "base/check_is_test.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/containers/flat_map.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/mac/mac_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/not_fatal_until.h"
#include "base/run_loop.h"
#include "base/scoped_multi_source_observation.h"
#include "base/scoped_observation.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "build/branding_buildflags.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/apps/app_shim/app_shim_termination_manager.h"
#include "chrome/browser/apps/platform_apps/app_window_registry_util.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/command_updater_impl.h"
#include "chrome/browser/download/download_core_service.h"
#include "chrome/browser/download/download_core_service_factory.h"
#include "chrome/browser/enterprise/isolated_mode/isolated_mode_settings_service_factory.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/first_run/first_run.h"
#include "chrome/browser/global_keyboard_shortcuts_mac.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/browser/lifetime/application_lifetime_desktop.h"
#include "chrome/browser/lifetime/browser_shutdown.h"
#include "chrome/browser/mac/auth_session_request.h"
#include "chrome/browser/mac/key_window_notifier.h"
#include "chrome/browser/policy/chrome_browser_policy_connector.h"
#include "chrome/browser/prefs/incognito_mode_prefs.h"
#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_attributes_entry.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_manager_observer.h"
#include "chrome/browser/profiles/profile_observer.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/sessions/session_restore.h"
#include "chrome/browser/sessions/session_service.h"
#include "chrome/browser/sessions/session_service_factory.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/shortcuts/chrome_webloc_file.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/task_manager/task_manager_metrics_recorder.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_command_controller.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_live_tab_context.h"
#include "chrome/browser/ui/browser_mac.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/browser_window/public/create_browser_window.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/browser_window/public/profile_browser_collection.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/cocoa/apps/quit_with_apps_controller_mac.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
#import "chrome/browser/ui/cocoa/confirm_quit.h"
#import "chrome/browser/ui/cocoa/confirm_quit_panel_controller.h"
#include "chrome/browser/ui/cocoa/handoff_observer.h"
#import "chrome/browser/ui/cocoa/history_menu_bridge.h"
#import "chrome/browser/ui/cocoa/profiles/profile_menu_controller.h"
#import "chrome/browser/ui/cocoa/share_menu_controller.h"
#import "chrome/browser/ui/cocoa/tab_menu_bridge.h"
#include "chrome/browser/ui/dialogs/browser_dialogs.h"
#include "chrome/browser/ui/extensions/application_launch.h"
#include "chrome/browser/ui/profiles/profile_picker.h"
#include "chrome/browser/ui/startup/first_run_service.h"
#include "chrome/browser/ui/startup/google_chrome_scheme_util.h"
#include "chrome/browser/ui/startup/startup_browser_creator.h"
#include "chrome/browser/ui/startup/startup_browser_creator_impl.h"
#include "chrome/browser/ui/startup/startup_tab.h"
#include "chrome/browser/ui/startup/startup_types.h"
#include "chrome/browser/ui/startup/url_util.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/color_provider_browser_helper.h"
#include "chrome/browser/ui/webui/util/webui_util_desktop.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/common/channel_info.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths_internal.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/mac/app_mode_common.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/branded_strings.h"
#include "chrome/grit/generated_resources.h"
#include "components/enterprise/browser/controller/chrome_browser_cloud_management_controller.h"
#include "components/handoff/handoff_manager.h"
#include "components/handoff/handoff_utility.h"
#include "components/keep_alive_registry/keep_alive_registry.h"
#include "components/keep_alive_registry/keep_alive_types.h"
#include "components/keep_alive_registry/scoped_keep_alive.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/sessions/core/tab_restore_service.h"
#include "content/public/browser/download_manager.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/buildflags/buildflags.h"
#include "net/base/apple/url_conversions.h"
#include "net/base/filename_util.h"
#import "ui/base/cocoa/nsmenu_additions.h"
#import "ui/base/cocoa/nsmenuitem_additions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_manager.h"
#include "ui/color/color_provider_source.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/menus/cocoa/menu_controller.h"
#include "ui/native_theme/native_theme.h"
#include "url/gurl.h"

namespace {

// True while AppController is calling chrome::NewEmptyWindow(). We need a
// global flag here, analogue to StartupBrowserCreator::InProcessStartup()
// because otherwise the SessionService will try to restore sessions when we
// make a new window while there are no other active windows.
bool g_is_opening_new_window = false;

// Stores the pending web auth requests (typically while the profile is being
// loaded) until they are passed to the AuthSessionRequest class.
NSMutableDictionary<NSUUID*, ASWebAuthenticationSessionRequest*>*
GetPendingWebAuthRequests() {
  static NSMutableDictionary* g_pending_requests =
      [[NSMutableDictionary alloc] init];
  return g_pending_requests;
}

// Returns true if the profile requires signin before being used.
bool IsProfileSignedOut(const base::FilePath& profile_path);

// Starts a web authentication session request.
void BeginHandlingWebAuthenticationSessionRequestWithProfile(
    ASWebAuthenticationSessionRequest* request,
    Profile* profile) {
  NSUUID* key = request.UUID;
  if (![GetPendingWebAuthRequests() objectForKey:key]) {
    return;  // The request has been canceled, do not start the session.
  }

  [GetPendingWebAuthRequests() removeObjectForKey:key];

  // If there is no safe profile, |profile| is nullptr, and the session will
  // fail immediately.
  AuthSessionRequest::StartNewAuthSession(request, profile);
}

// Activates a browser window having the given profile (the last one active) if
// possible and returns a pointer to the activated browser or NULL if this was
// not possible. If the last active browser is minimized (in particular, if
// there are only minimized windows), it will unminimize it.
BrowserWindowInterface* ActivateBrowser(Profile* profile) {
  ProfileBrowserCollection* collection =
      ProfileBrowserCollection::GetForProfile(
          profile->IsGuestSession()
              ? profile->GetPrimaryOTRProfile(/*create_if_needed=*/true)
              : profile);
  BrowserWindowInterface* browser =
      collection ? collection->GetLastActiveBrowser() : nullptr;

  if (browser) {
    browser = webui::GetBrowserForOpeningWebUi(browser);
  }

  if (browser) {
    browser->GetWindow()->Activate();
  }
  return browser;
}

// Launches a browser window associated with |profile|. Checks if we are in the
// first run of Chrome to decide if we need to launch a browser or not.
// The profile can be `nullptr` and in that case the last-used profile will be
// used.
void LaunchBrowserStartup(Profile* profile) {
  if (ProfilePicker::GetStartupMode() == StartupProfileMode::kProfilePicker) {
    ProfilePicker::Show(ProfilePicker::Params::FromEntryPoint(
        ProfilePicker::EntryPoint::kNewSessionOnExistingProcess));
    return;
  }
  CHECK(profile);

  base::AutoReset<bool> auto_reset_in_run(&g_is_opening_new_window, true);
  StartupBrowserCreator browser_creator;

  // For user-initiated launches (e.g. Dock icon click), we use a fresh command
  // line if SmartRestart is enabled. This prevents the new window from being
  // suppressed by any --no-startup-window flag that might have been used during
  // the initial background restart.
  base::CommandLine command_line =
      base::FeatureList::IsEnabled(features::kSmartRestart)
          ? base::CommandLine(
                base::CommandLine::ForCurrentProcess()->GetProgram())
          : *base::CommandLine::ForCurrentProcess();

  browser_creator.LaunchBrowser(command_line, profile, base::FilePath(),
                                chrome::startup::IsProcessStartup::kNo,
                                chrome::startup::IsFirstRun::kYes,
                                /*restore_tabbed_browser=*/true);
}

// Creates an empty browser window with the given profile and returns a pointer
// to the new |BrowserWindowInterface|.
BrowserWindowInterface* CreateBrowser(Profile* profile) {
  // Closes the first run if we open a new window.
  if (auto* fre_service =
          FirstRunServiceFactory::GetForBrowserContextIfExists(profile)) {
    fre_service->FinishFirstRunWithoutResumeTask();
  }

  {
    base::AutoReset<bool> auto_reset_in_run(&g_is_opening_new_window, true);
    chrome::NewEmptyWindow(profile);
  }

  BrowserWindowInterface* browser =
      GlobalBrowserCollection::GetInstance()->GetLastActiveBrowser();
  CHECK(browser);
  return browser;
}

// Activates a browser window having the given profile (the last one active) if
// possible or creates an empty one if necessary. Returns a pointer to the
// activated/new |BrowserWindowInterface|.
BrowserWindowInterface* ActivateOrCreateBrowser(Profile* profile) {
  if (BrowserWindowInterface* browser = ActivateBrowser(profile)) {
    return browser;
  }
  return CreateBrowser(profile);
}

// Attempts restoring a previous session if there is one. Otherwise, opens
// either the profile picker or a new browser, depending on user preferences.
void AttemptSessionRestore(Profile* profile) {
  if (!profile) {
    return;
  }
  DCHECK(!profile->IsGuestSession());
  DCHECK(!IsProfileSignedOut(profile->GetPath()));
  SessionService* sessionService =
      SessionServiceFactory::GetForProfileForSessionRestore(profile);
  if (sessionService &&
      sessionService->RestoreIfNecessary(StartupTabs(),
                                         /*restore_apps=*/false)) {
    // Session was restored.
    return;
  }
  // No session to restore, proceed with normal startup.
  LaunchBrowserStartup(profile);
}

// Record the location of the application bundle (containing the main framework)
// from which Chromium was loaded. This is used by app mode shims to find
// Chromium.
void RecordLastRunAppBundlePath() {
  // Going up three levels from |chrome::GetVersionedDirectory()| gives the
  // real, user-visible app bundle directory. (The alternatives give either the
  // framework's path or the initial app's path, which may be an app mode shim
  // or a unit test.)
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  // Go up five levels from the versioned sub-directory of the framework, which
  // is at C.app/Contents/Frameworks/C.framework/Versions/V.
  base::FilePath app_bundle_path = chrome::GetFrameworkBundlePath()
                                       .DirName()
                                       .DirName()
                                       .DirName()
                                       .DirName()
                                       .DirName();
  base::apple::ScopedCFTypeRef<CFStringRef> app_bundle_path_cfstring =
      base::apple::FilePathToCFString(app_bundle_path);
  CFPreferencesSetAppValue(
      base::apple::NSToCFPtrCast(app_mode::kLastRunAppBundlePathPrefsKey),
      app_bundle_path_cfstring.get(),
      base::SysUTF8ToCFStringRef(base::apple::BaseBundleID()).get());
}

bool IsProfileSignedOut(const base::FilePath& profile_path) {
  ProfileAttributesEntry* entry =
      g_browser_process->profile_manager()
          ->GetProfileAttributesStorage()
          .GetProfileAttributesWithPath(profile_path);
  return entry && entry->IsSigninRequired();
}

void ConfigureNSAppForKioskMode() {
  NSApp.presentationOptions =
      NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar |
      NSApplicationPresentationDisableProcessSwitching |
      NSApplicationPresentationDisableSessionTermination |
      NSApplicationPresentationDisableForceQuit |
      NSApplicationPresentationFullScreen;
}

// Returns the list of windows for all browser windows (excluding apps).
NSSet<NSWindow*>* GetBrowserWindows() {
  NSMutableSet<NSWindow*>* result = [NSMutableSet set];
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [result](BrowserWindowInterface* browser_window_interface) {
        // When focusing Chrome, don't focus any browser windows associated with
        // an app (https://crbug.com/40626510).
        if (browser_window_interface->GetType() ==
            BrowserWindowInterface::TYPE_APP) {
          return true;
        }
        [result addObject:browser_window_interface->GetWindow()
                              ->GetNativeWindow()
                              .GetNativeNSWindow()];
        return true;
      });
  return result;
}

void FocusWindowSetOnCurrentSpace(NSSet<NSWindow*>* windows) {
  // This callback runs before AppKit picks its own window to
  // deminiaturize, so we get to pick one from the right set. Limit to
  // the windows on the current workspace. Otherwise we jump spaces
  // haphazardly.
  //
  // Also consider both visible and hidden windows; this call races
  // with the system unhiding the application. http://crbug.com/41104339
  //
  // NOTE: If this is called in the
  // -applicationShouldHandleReopen:hasVisibleWindows: hook when
  // clicking the dock icon, and that caused macOS to begin switch
  // spaces, isOnActiveSpace gives the answer for the PREVIOUS
  // space. This means that we actually raise and focus the wrong
  // space's windows, leaving the new key window off-screen. To detect
  // this, check if the key window is on the active space prior to
  // calling.
  //
  // Also, if we decide to deminiaturize a window during a space switch,
  // that can switch spaces and then switch back. Fortunately, this only
  // happens if, say, space 1 contains an app, space 2 contains a
  // miniaturized browser. We click the icon, macOS switches to space 1,
  // we deminiaturize the browser, and that triggers switching back.
  //
  // TODO(davidben): To limit those cases, consider preferentially
  // deminiaturizing a window on the current space.
  NSWindow* frontmost_window = nil;
  NSWindow* frontmost_miniaturized_window = nil;
  bool all_miniaturized = true;
  for (NSWindow* win in [NSApp.orderedWindows reverseObjectEnumerator]) {
    if (![windows containsObject:win]) {
      continue;
    }
    if (win.miniaturized) {
      frontmost_miniaturized_window = win;
    } else if (win.visible) {
      all_miniaturized = false;
      if (win.onActiveSpace) {
        // Raise the old |frontmost_window| (if any). The topmost |win| will be
        // raised with makeKeyAndOrderFront: below.
        [frontmost_window orderFront:nil];
        frontmost_window = win;
      }
    }
  }
  if (all_miniaturized && frontmost_miniaturized_window) {
    DCHECK(!frontmost_window);
    // Note the call to makeKeyAndOrderFront: will deminiaturize the window.
    frontmost_window = frontmost_miniaturized_window;
  }

  if (frontmost_window) {
    // Use deminiaturize when the window is minimized to avoid the issue where
    // the window suddenly appears before the animation starts during
    // restoration.
    if (frontmost_window.miniaturized) {
      [frontmost_window deminiaturize:nil];
    } else {
      [frontmost_window makeKeyAndOrderFront:nil];
    }
    [NSApp activateIgnoringOtherApps:YES];
  }
}

// Returns the profile path to be used at startup.
base::FilePath GetStartupProfilePathMac() {
  // This profile path is used to open URLs passed in application:openURLs: and
  // should not default to Guest when the profile picker is shown.
  // TODO(crbug.com/40159795): Remove the ignore_profile_picker parameter
  // once the picker supports opening URLs.
  StartupProfilePathInfo profile_path_info = GetStartupProfilePath(
      /*cur_dir=*/base::FilePath(), *base::CommandLine::ForCurrentProcess(),
      /*ignore_profile_picker=*/true);
  DCHECK_EQ(profile_path_info.mode, StartupProfileMode::kBrowserWindow);
  return profile_path_info.path;
}

void OpenStartupTabsInBrowserWithProfile(const StartupTabs& tabs,
                                         Profile* profile);

void OpenStartupTabsInBrowser(StartupTabs tabs) {
  StartupTabs regular_tabs;
  std::vector<base::FilePath> shortcuts;

  for (auto& tab : tabs) {
    base::FilePath path;
    if (net::FileURLToFilePath(tab.url, &path) &&
        path.Extension() == shortcuts::ChromeWeblocFile::kFileExtension) {
      shortcuts.push_back(path);
    } else {
      regular_tabs.push_back(std::move(tab));
    }
  }

  if (!shortcuts.empty()) {
    // Parse/read the shortcut files on the thread pool to avoid blocking the
    // UI thread.
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE,
        {base::MayBlock(), base::TaskPriority::USER_BLOCKING,
         base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
        base::BindOnce(
            [](const std::vector<base::FilePath>& shortcuts) {
              base::flat_map<base::FilePath, StartupTabs> profile_tab_map;
              for (const auto& path : shortcuts) {
                auto shortcut = shortcuts::ChromeWeblocFile::LoadFromFile(path);
                // TODO: Consider opening the original file URL?
                if (!shortcut.has_value()) {
                  continue;
                }
                bool is_shortcut_url_valid =
                    startup::ValidateLaunchUrlWebUnsafe(shortcut->target_url());
#if BUILDFLAG(ENABLE_EXTENSIONS)
                is_shortcut_url_valid =
                    is_shortcut_url_valid || shortcut->target_url().SchemeIs(
                                                 extensions::kExtensionScheme);
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)
                if (!is_shortcut_url_valid) {
                  LOG(ERROR) << "Not allowed to open target url: "
                             << shortcut->target_url();
                  continue;
                }
                profile_tab_map[shortcut->profile_path_name().path()]
                    .emplace_back(shortcut->target_url(),
                                  /*is_untrusted_launch=*/false);
              }
              return profile_tab_map;
            },
            std::move(shortcuts)),
        base::BindOnce([](const base::flat_map<base::FilePath, StartupTabs>
                              profile_tab_map) {
          const base::FilePath& user_data_dir =
              g_browser_process->profile_manager()->user_data_dir();
          ProfileAttributesStorage& profile_attributes_storage =
              g_browser_process->profile_manager()
                  ->GetProfileAttributesStorage();
          for (const auto& [profile, tabs_for_profile] : profile_tab_map) {
            const base::FilePath profile_path = user_data_dir.Append(profile);
            if (profile_attributes_storage.GetProfileAttributesWithPath(
                    profile_path)) {
              app_controller_mac::RunInProfileSafely(
                  profile_path,
                  base::BindOnce(&OpenStartupTabsInBrowserWithProfile,
                                 tabs_for_profile),
                  app_controller_mac::kShowProfilePickerOnFailure);
            } else {
              // If the target profile doesn't exist, fall back to the
              // last profile.
              app_controller_mac::RunInLastProfileSafely(
                  base::BindOnce(&OpenStartupTabsInBrowserWithProfile,
                                 tabs_for_profile),
                  app_controller_mac::kShowProfilePickerOnFailure);
            }
          }
        }));
  }

  if (!regular_tabs.empty()) {
    app_controller_mac::RunInLastProfileSafely(
        base::BindOnce(&OpenStartupTabsInBrowserWithProfile, regular_tabs),
        app_controller_mac::kShowProfilePickerOnFailure);
  }
}

}  // namespace

// This is extracted as a standalone function in order
// to be friend with base::ScopedAllowBlocking.
Profile* GetLastProfileMac() {
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  if (!profile_manager)
    return nullptr;

  base::FilePath profile_path = GetStartupProfilePathMac();
  // ProfileManager::GetProfile() is blocking if the profile was not loaded yet.
  // TODO(crbug.com/40054768): Change this code to return nullptr when
  // the profile is not loaded, and update all callers to handle this case.
  base::ScopedAllowBlocking allow_blocking;
  return profile_manager->GetProfile(profile_path);
}

@interface AppController () <HandoffObserverDelegate>
- (void)initMenuState;
- (void)initProfileMenu;
- (void)updateConfirmToQuitPrefMenuItem:(NSMenuItem*)item;
- (void)cancelConfirmQuitPanel;
- (void)registerServicesMenuTypesTo:(NSApplication*)app;
- (void)checkForAnyKeyWindows;
- (BOOL)userWillWaitForInProgressDownloads:(int)downloadCount;
- (BOOL)shouldQuitWithInProgressDownloads;
- (void)profileWasRemoved:(const base::FilePath&)profilePath
             forIncognito:(bool)isIncognito;

// This class cannot open tabs until startup has finished. The tabs that cannot
// be opened are cached in |_startupTabs|. This method must be called exactly
// once after startup has completed. It opens the tabs in |_startupTabs|, and
// clears |_startupTabs|.
- (void)openStartupUrls;

// Opens a tab for each StartupTab in |tabs|. If there is exactly one tab open
// before this method is called, and that tab is the NTP, then this method
// closes the NTP after all the |tabs| have been opened.
- (void)openStartupTabsReplacingNTP:(StartupTabs)tabs;

// Returns |YES| if |webContents| can be sent to another device via Handoff.
- (BOOL)isHandoffEligible:(content::WebContents*)webContents;

// This method passes |handoffURL| and |handoffTitle| to |handoffManager_|.
// This is a separate method (vs. being inlined into `updateHandoffManager`
// below) so that it can be swizzled in tests.
- (void)updateHandoffManagerWithURL:(const GURL&)handoffURL
                              title:(const std::u16string&)handoffTitle;

// Lazily creates the Handoff Manager. Updates the state of the Handoff
// Manager. This method is idempotent. This should be called:
// - During initialization.
// - When the current tab navigates to a new URL.
// - When the active browser changes.
// - When the active browser's active tab switches.
// |webContents| should be the new, active WebContents.
- (void)updateHandoffManager:(content::WebContents*)webContents;

// Return false if Chrome startup is paused by dialog and AppController is
// called without any initialized Profile.
- (BOOL)isProfileReady;

// Reset `_keepAlive` if Chrome is running in hidden mode, recreating it when
// Chrome is no longer hidden.
- (void)resetKeepAliveWhileHidden;

// A callback that updates the relevant commands in the tab menu that depend on
// position of the tab strip.
- (void)onVerticalTabStripModeChanged:
    (tabs::VerticalTabStripStateController*)stateController;
@end

class AppControllerProfileObserver : public ProfileAttributesStorage::Observer,
                                     public ProfileManagerObserver,
                                     public ProfileObserver {
 public:
  AppControllerProfileObserver(
      ProfileManager* profile_manager, AppController* app_controller)
      : profile_manager_(profile_manager),
        app_controller_(app_controller) {
    DCHECK(profile_manager_);
    DCHECK(app_controller_);
    // Listen to ProfileObserver and ProfileManagerObserver.
    profile_manager_observer_.Observe(profile_manager_.get());
    for (Profile* profile : profile_manager_->GetLoadedProfiles()) {
      profile_observers_.AddObservation(profile);
      Profile* otr_profile =
          profile->GetPrimaryOTRProfile(/*create_if_needed=*/false);
      if (otr_profile) {
        profile_observers_.AddObservation(otr_profile);
      }
    }
    storage_observer_.Observe(&profile_manager_->GetProfileAttributesStorage());
  }

  AppControllerProfileObserver(const AppControllerProfileObserver&) = delete;
  AppControllerProfileObserver& operator=(const AppControllerProfileObserver&) =
      delete;

  ~AppControllerProfileObserver() override = default;

 private:
  // ProfileAttributesStorage::Observer implementation:

  // `ProfileAttributesStorage::Observer::OnProfileAdded()` must be explicitly
  // defined even if it's empty, because of the competing overload
  // `ProfileManager::Observer::OnProfileAdded()`.
  void OnProfileAdded(const base::FilePath& profile_path) override {}

  void OnProfileWasRemoved(const base::FilePath& profile_path,
                           const std::u16string& profile_name) override {
    // When a profile is deleted we need to notify the AppController,
    // so it can correctly update its pointer to the last used profile.
    [app_controller_ profileWasRemoved:profile_path forIncognito:false];
  }

  // ProfileManager::Observer implementation:
  void OnProfileAdded(Profile* profile) override {
    profile_observers_.AddObservation(profile);
  }

  // ProfileObserver implementation:
  void OnProfileWillBeDestroyed(Profile* profile) override {
    profile_observers_.RemoveObservation(profile);

    bool is_profile_observed =
        profile->IsOffTheRecord() || ObserveRegularProfiles();

    // If the profile is not observed, then no need to call rest.
    if (!is_profile_observed)
      return;

    [app_controller_ profileWasRemoved:profile->GetPath()
                          forIncognito:profile->IsOffTheRecord()];
  }

  void OnOffTheRecordProfileCreated(Profile* off_the_record) override {
    profile_observers_.AddObservation(off_the_record);
  }

  static bool ObserveRegularProfiles() {
    return base::FeatureList::IsEnabled(
        features::kDestroyProfileOnBrowserClose);
  }

  base::ScopedMultiSourceObservation<Profile, ProfileObserver>
      profile_observers_{this};
  base::ScopedObservation<ProfileAttributesStorage,
                          ProfileAttributesStorage::Observer>
      storage_observer_{this};
  base::ScopedObservation<ProfileManager, ProfileManagerObserver>
      profile_manager_observer_{this};

  const raw_ptr<ProfileManager> profile_manager_;
  AppController* const app_controller_;  // Weak; owns us.
};

@implementation AppController {
  // Manages the state of the command menu items.
  std::unique_ptr<CommandUpdater> _menuState;

  // The profile last used by a Browser. It is this profile that was used to
  // build the user-data specific main menu items.
  raw_ptr<Profile, DanglingUntriaged> _lastProfile;

  // The ProfileObserver observes the ProfileAttributesStorage and gets notified
  // when a profile has been deleted.
  std::unique_ptr<AppControllerProfileObserver>
      _profileAttributesStorageObserver;

  // Management of the bookmark menu which spans across all windows
  // (and Browser*s). |profileBookmarkMenuBridgeMap_| is a cache that owns one
  // pointer to a BookmarkMenuBridge for each profile. |bookmarkMenuBridge_| is
  // a weak pointer that is updated to match the corresponding cache entry
  // during a profile switch.
  raw_ptr<BookmarkMenuBridge, DanglingUntriaged> _bookmarkMenuBridge;
  std::map<base::FilePath, std::unique_ptr<BookmarkMenuBridge>>
      _profileBookmarkMenuBridgeMap;

  std::unique_ptr<HistoryMenuBridge> _historyMenuBridge;


  // The profile menu, which appears right before the Help menu. It is only
  // available when multiple profiles is enabled.
  ProfileMenuController* __strong _profileMenuController;

  // Controller for the macOS system share menu.
  ShareMenuController* __strong _shareMenuController;

  std::unique_ptr<TabMenuBridge> _tabMenuBridge;

  // If we're told to open URLs (in particular, via |-application:openURLs:| by
  // Launch Services) before we've launched the browser, we queue them up in
  // |_startupTabs| so that they can go in the first browser window/tab.
  StartupTabs _startupTabs;
  BOOL _startupComplete;

  // Outlets for testing close tab/window menu items.
  NSMenuItem* __strong _cmdWMenuItemForTesting;
  NSMenuItem* __strong _shiftCmdWMenuItemForTesting;
  NSWindow* __strong _mainWindowForTesting;

  std::unique_ptr<PrefChangeRegistrar> _profilePrefRegistrar;
  PrefChangeRegistrar _localPrefRegistrar;

  // Displays a notification when quitting while apps are running.
  scoped_refptr<QuitWithAppsController> _quitWithAppsController;

  // Responsible for maintaining all state related to the Handoff feature.
  HandoffManager* __strong _handoffManager;

  // Observes changes to the active web contents.
  std::unique_ptr<HandoffObserver> _handoffObserver;

  // This will be true after receiving a NSWorkspaceWillPowerOffNotification.
  BOOL _isPoweringOff;

  // This will be true after receiving a |-applicationWillTerminate:| event.
  BOOL _isShuttingDown;

  // Request to keep the browser alive during that object's lifetime. Every
  // Browser instance holds a ScopedKeepAlive as well to make sure the browser
  // stays alive as long as any windows are open, but on macOS we want the
  // browser process to also stay alive without any windows open. To support
  // this, this ScopedKeepAlive is created in -applicationDidFinishLaunching.
  //
  // When the user launches an app shim while Chrome isn't running, the app shim
  // launches Chrome with the _kLSOpenOptionBackgroundLaunchKey option. This
  // causes the activationPolicy to be equal to
  // NSApplicationActivationPolicyProhibited (i.e. Chrome is not visibly running
  // in the Dock and task switcher). In this state we don't want to keep the
  // browser process alive indefinitely (but can't skip creating this
  // ScopedKeepAlive entirely, as that could result in the  browser process
  // terminating before it has fully initialized and had a chance to for example
  // process messages from app shims). Once the app shim launch has been
  // processed to the point of having created the expected Browser instances or
  // other ScopedKeepAlive instances, -resetKeepAliveWhileHidden is called to
  // reset `_keepAlive`.
  //
  // When the user explicitly launches Chrome, or when Chrome creates any
  // windows, the `activationPolicy` is changed by the OS. By observing
  // `activationPolicy` for `NSRunningApplication.currentApplication` we can
  // recreate `_keepAlive` if and when the activation policy changes.
  std::unique_ptr<ScopedKeepAlive> _keepAlive;

  // Set to `NSRunningApplication.currentApplication` while we're observing
  // the `activationPolicy` of ourselves.
  NSRunningApplication* __strong _runningApplication;

  // Remembers whether _lastProfile had TabRestoreService entries. This is saved
  // when _lastProfile is destroyed and Chromium enters the zero-profile state.
  //
  // By remembering this bit, Chromium knows whether to enable or disable
  // Cmd+Shift+T and the related "File > Reopen Closed Tab" entry.
  BOOL _tabRestoreWasEnabled;

  // Callback subscription that notifies when the mode of the vertical tab strip
  // state controller changes.
  base::CallbackListSubscription _verticalTabSubscription;

  // The last active browser, used to query its ColorProvider on demand.
  // WeakPtr self-nulls on browser destruction.
  base::WeakPtr<BrowserWindowInterface> _lastActiveBrowser;

  // The controller that manages the "Confirm Quit" HUD. This is lazily
  // initialized on the first keyboard-initiated quit attempt and cleared when
  // the panel is dismissed or the quit is canceled.
  ConfirmQuitPanelController* __strong _confirmQuitPanelController;
}

@synthesize startupComplete = _startupComplete;

+ (AppController*)sharedController {
  static AppController* sharedController = [] {
    AppController* sharedController = [[AppController alloc] init];
    NSApp.delegate = sharedController;
    return sharedController;
  }();

  CHECK_NE(nil, sharedController);
  CHECK_EQ(NSApp.delegate, sharedController);
  return sharedController;
}

- (instancetype)init {
  if (self = [super init]) {
    // -[NSMenu cr_menuItemForKeyEquivalentEvent:] lives in /content, but
    // we need to execute special update code before the search begins.
    // Setting this block gives us the hook we need.
    [NSMenu cr_setMenuItemForKeyEquivalentEventPreSearchBlock:^{
      // We avoid calling -[NSMenuDelegate menuNeedsUpdate:] on each submenu's
      // delegate as that can be slow. Instead, we update the relevant
      // NSMenuItems.
      [AppController.sharedController updateMenuItemKeyEquivalents];
    }];

    // Create a keep-alive to keep the application running so it doesn't go away
    // when all the browser windows get closed. This is done as early as
    // possible to make sure we even keep the application alive if some other
    // subsystem creates and destroys a ScopedKeepAlive before the application
    // finishes launching.
    _keepAlive = std::make_unique<ScopedKeepAlive>(
        KeepAliveOrigin::APP_CONTROLLER, KeepAliveRestartOption::DISABLED);
  }
  return self;
}

- (void)dealloc {
  NOTREACHED();
}

- (NSMenu*)fileMenu {
  return [[NSApp.mainMenu itemWithTag:IDC_FILE_MENU] submenu];
}

// Returns the ⌘W menu item in the File menu. Returns nil if no such menu item
// exists (e.g. the user has custom shortcut settings).

- (NSMenuItem*)cmdWMenuItem {
  NSArray* fileMenuItemArray = [self fileMenu].itemArray;
  if (_cmdWMenuItemForTesting) {
    fileMenuItemArray = @[ _cmdWMenuItemForTesting ];
  }

  NSMenuItem* cmdWMenuItem = nil;

  for (NSMenuItem* item in fileMenuItemArray) {
    if ([@"w" isEqualToString:item.keyEquivalent] &&
        item.keyEquivalentModifierMask == NSEventModifierFlagCommand) {
      cmdWMenuItem = item;
      break;
    }
  }

  // Make sure the user hasn't reassigned ⌘W.
  if (cmdWMenuItem.tag != 0 && cmdWMenuItem.tag != IDC_CLOSE_WINDOW &&
      cmdWMenuItem.tag != IDC_CLOSE_TAB) {
    return nil;
  }

  return cmdWMenuItem;
}

// Returns the ⇧⌘W menu item in the File menu. Returns nil if no such menu item
// exists (e.g. the user has custom shortcut settings).
- (NSMenuItem*)shiftCmdWMenuItem {
  NSArray* fileMenuItemArray = [self fileMenu].itemArray;
  if (_shiftCmdWMenuItemForTesting) {
    fileMenuItemArray = @[ _shiftCmdWMenuItemForTesting ];
  }

  NSMenuItem* shiftCmdWMenuItem = nil;

  for (NSMenuItem* item in fileMenuItemArray) {
    // "Shift" is part of the keyEquivalent. It doesn't live in the modifier
    // mask.
    if ([@"W" isEqualToString:item.keyEquivalent] &&
        item.keyEquivalentModifierMask == NSEventModifierFlagCommand) {
      shiftCmdWMenuItem = item;
      break;
    }
  }

  // Make sure the user hasn't reassigned ⇧⌘W.
  if (shiftCmdWMenuItem.tag != 0 && shiftCmdWMenuItem.tag != IDC_CLOSE_WINDOW &&
      shiftCmdWMenuItem.tag != IDC_CLOSE_TAB) {
    return nil;
  }

  return shiftCmdWMenuItem;
}

// This method is called very early in application startup (ie, before
// the profile is loaded or any preferences have been registered). Defer any
// user-data initialization until -applicationDidFinishLaunching:.
- (void)mainMenuCreated {
  NSNotificationCenter* notificationCenter = NSNotificationCenter.defaultCenter;
  [notificationCenter
      addObserver:self
         selector:@selector(windowDidResignKey:)
             name:NSWindowDidResignKeyNotification
           object:nil];
  [notificationCenter
      addObserver:self
         selector:@selector(windowDidBecomeMain:)
             name:NSWindowDidBecomeMainNotification
           object:nil];

  [NSWorkspace.sharedWorkspace.notificationCenter
      addObserver:self
         selector:@selector(willPowerOff:)
             name:NSWorkspaceWillPowerOffNotification
           object:nil];

  // Set up the command updater for when there are no windows open
  [self initMenuState];

  // Initialize the Profile menu.
  [self initProfileMenu];
}

- (void)unregisterEventHandlers {
  [NSNotificationCenter.defaultCenter removeObserver:self];
  [NSWorkspace.sharedWorkspace.notificationCenter removeObserver:self];
}

// (NSApplicationDelegate protocol) This is the Apple-approved place to override
// the default handlers.
- (void)applicationWillFinishLaunching:(NSNotification*)notification {
  NSWindow.allowsAutomaticWindowTabbing = NO;

  [self initShareMenu];
}

- (void)tryToTerminateApplication {
  if ([self confirmQuitIfNeeded] == ConfirmQuitResultAborted) {
    return;
  }

  // If termination is already underway (and this is a redundant attempt to
  // quit) then there's nothing to be done.
  if (browser_shutdown::IsTryingToQuit()) {
    return;
  }

  // Reset this now that we've received the call to terminate.
  BOOL isPoweringOff = _isPoweringOff;
  _isPoweringOff = NO;

  // Stop the browser from re-opening when we close Chrome while
  // in the first run experience.
  if (auto* profile = self.lastProfileIfLoaded) {
    if (auto* freService =
            FirstRunServiceFactory::GetForBrowserContextIfExists(profile)) {
      freService->FinishFirstRunWithoutResumeTask();
    }
  }

  // Check for in-process downloads, and prompt the user if they really want
  // to quit (and thus cancel downloads).
  if (![self shouldQuitWithInProgressDownloads]) {
    return;
  }

  // Check for active apps. If quitting is prevented, only close browsers and
  // sessions.
  if (!isPoweringOff && _quitWithAppsController.get() &&
      !_quitWithAppsController->ShouldQuit()) {
    chrome::OnClosingAllBrowsers(true);
    // This will close all browser sessions.
    chrome::CloseAllBrowsers();

    // At this point, the user has already chosen to cancel downloads. If we
    // were to shut down as usual, the downloads would be cancelled in
    // DownloadCoreService::Shutdown().
    DownloadCoreService::CancelAllDownloads(
        DownloadCoreService::CancelDownloadsTrigger::kShutdown);

    return;
  }

  // Initiate the shutdown.
  chrome::OnClosingAllBrowsers(true);
  chrome::CloseAllBrowsersAndQuit();
}

- (void)cancelConfirmQuitPanel {
  [_confirmQuitPanelController cancel];
  _confirmQuitPanelController = nil;
}

- (void)stopTryingToTerminateApplication {
  if (browser_shutdown::IsTryingToQuit()) {
    // Reset the "trying to quit" state, so that closing all browser windows
    // will no longer lead to termination.
    browser_shutdown::SetTryingToQuit(false);
    [self cancelConfirmQuitPanel];
  }
}

- (void)allowApplicationToTerminate {
  // Reset the keep-alive to stop the RunLoop and terminate the application when
  // the last Browser is closed.
  _keepAlive.reset();
}

- (ConfirmQuitResult)confirmQuitIfNeeded {
  // If there are no windows, quit immediately.
  if (!GetLastActiveBrowserWindowInterfaceWithAnyProfile() &&
      !AppWindowRegistryUtil::IsAppWindowVisibleInAnyProfile(0)) {
    return ConfirmQuitResultNotPrompted;
  }

  // Check if the preference is turned on.
  const PrefService* prefs = g_browser_process->local_state();
  if (!prefs->GetBoolean(prefs::kConfirmToQuitEnabled)) {
    confirm_quit::RecordHistogram(confirm_quit::kNoConfirm);
    return ConfirmQuitResultNotPrompted;
  }

  NSEvent* event = [NSApp currentEvent];
  // Run only for keyboard-initiated quits.
  if (!event || CommandForKeyEvent(event).chrome_command != IDC_EXIT) {
    return ConfirmQuitResultNotPrompted;
  }

  // In the event of a double-quit attempt (holding command and pressing 'Q'
  // twice), we will already have a ConfirmQuitPanelController, so only create a
  // new one if needed.
  if (!_confirmQuitPanelController) {
    _confirmQuitPanelController = [[ConfirmQuitPanelController alloc] init];
  }

  __weak AppController* weakSelf = self;
  BOOL quitConfirmed = [_confirmQuitPanelController
      runConfirmQuitLoopWithEvent:event
                dismissedCallback:^{
                  AppController* strongSelf = weakSelf;
                  if (strongSelf && !browser_shutdown::IsTryingToQuit()) {
                    strongSelf->_confirmQuitPanelController = nil;
                  }
                }];

  return quitConfirmed ? ConfirmQuitResultConfirmed : ConfirmQuitResultAborted;
}

// Handles the NSApplicationWillTerminateNotification notification. (Note to
// reader: this notification was posted from application_lifetime_mac.mm in
// HandleAppExitingForPlatform(), not from within AppKit!) At this point,
// termination will happen, so tear everything down.
- (void)applicationWillTerminate:(NSNotification*)aNotification {
  // There better be no browser windows left at this point.
  CHECK(!KeepAliveRegistry::GetInstance()->IsOriginRegistered(
      KeepAliveOrigin::BROWSER));

  // Reset the keep-alive that has been keeping the browser process alive. Once
  // all the browsers get dealloc'd, it will stop the RunLoop and fall back into
  // main().
  _keepAlive.reset();

  // Stop observing NSRunningApplication.
  if (_runningApplication) {
    [_runningApplication removeObserver:self
                             forKeyPath:@"activationPolicy"
                                context:nullptr];
  }

  // Reset local state watching, as this object outlives the prefs system.
  _localPrefRegistrar.RemoveAll();

  _isShuttingDown = true;

  // `_historyMenuBridge` has a dependency on `_lastProfile`, so that's why it's
  // deleted first.
  _historyMenuBridge.reset();


  // It's safe to delete |_lastProfile| now.
  [self setLastProfile:nullptr];

  _profileAttributesStorageObserver.reset();
  [self unregisterEventHandlers];
  _profileBookmarkMenuBridgeMap.clear();
}

- (void)didEndMainMessageLoop {
  if (!_lastProfile) {
    // If only the profile picker is open and closed again, there is no profile
    // loaded when main message loop ends and we cannot load it from disk now.
    return;
  }
  auto* browser_collection =
      ProfileBrowserCollection::GetForProfile(_lastProfile);
  DCHECK_EQ(0u, browser_collection ? browser_collection->GetSize() : 0u);
  if (!browser_collection || !browser_collection->GetSize()) {
    // As we're shutting down, we need to nuke the TabRestoreService, which
    // will start the shutdown of the NavigationControllers and allow for
    // proper shutdown. If we don't do this, Chrome won't shut down cleanly,
    // and may end up crashing when some thread tries to use the IO thread (or
    // another thread) that is no longer valid.
    TabRestoreServiceFactory::ResetForProfile(_lastProfile);
  }
}

// See if the focused window window has tabs, and adjust the key equivalents for
// Close Tab/Close Window accordingly.
- (void)menuNeedsUpdate:(NSMenu*)menu {
  DCHECK(menu == [self fileMenu]);
  [self updateMenuItemKeyEquivalents];
}

- (void)windowDidResignKey:(NSNotification*)notify {
  // If a window is closed, this notification is fired but |[NSApp keyWindow]|
  // returns nil regardless of whether any suitable candidates for the key
  // window remain. It seems that the new key window for the app is not set
  // until after this notification is fired, so a check is performed after the
  // run loop is allowed to spin.
  [self performSelector:@selector(checkForAnyKeyWindows)
             withObject:nil
             afterDelay:0.0];
}

- (void)windowDidBecomeMain:(NSNotification*)notify {
  BrowserWindowInterface* browser =
      GlobalBrowserCollection::GetInstance()->FindBrowserWithWindow(
          gfx::NativeWindow([notify object]));
  if (!browser)
    return;

  // Since we may have different windows with different profiles, we clear the
  // subscription each time a window becomes the main.
  _verticalTabSubscription = {};

  if (browser->GetType() == BrowserWindowInterface::Type::TYPE_NORMAL) {
    if (!_tabMenuBridge) {
      _tabMenuBridge = std::make_unique<TabMenuBridge>(
          [[NSApp mainMenu] itemWithTag:IDC_TAB_MENU]);
    }
    _tabMenuBridge->SetTabStripModel(browser->GetTabStripModel());

    if (auto* vertical_tab_strip_state_controller =
            tabs::VerticalTabStripStateController::From(browser)) {
      _verticalTabSubscription =
          vertical_tab_strip_state_controller->RegisterOnModeChanged(
              base::BindRepeating(
                  [](AppController* controller,
                     tabs::VerticalTabStripStateController* state_controller) {
                    [controller onVerticalTabStripModeChanged:state_controller];
                  },
                  self));
      // If the browser begins in VT mode, we want to ensure that we have the
      // correct text.
      [self onVerticalTabStripModeChanged:vertical_tab_strip_state_controller];
    }
  } else if (_tabMenuBridge) {
    _tabMenuBridge->SetTabStripModel(nullptr);
  }

  Profile* profile = browser->GetProfile();

  _lastActiveBrowser = browser->GetWeakPtr();
  [self setLastProfile:profile];
}

- (void)onVerticalTabStripModeChanged:
    (tabs::VerticalTabStripStateController*)stateController {
  if (!_tabMenuBridge) {
    return;
  }

  NSMenu* tabSubmenu = [[[NSApp mainMenu] itemWithTag:IDC_TAB_MENU] submenu];
  if (!tabSubmenu) {
    return;
  }

  bool enabled = stateController->ShouldDisplayVerticalTabs();

  auto updateMenuState = ^(NSInteger tag, bool enableVerticalTabs) {
    NSMutableArray<NSMenuItem*>* matchingItems = [NSMutableArray array];
    for (NSMenuItem* item in [tabSubmenu itemArray]) {
      if (item.tag == tag) {
        [matchingItems addObject:item];
      }
    }

    // There are 2 copies of "New Tab to Right" and "Close Tabs to Right"
    // for horizontal and vertical tab modes.
    CHECK_EQ(matchingItems.count, 2u);

    NSMenuItem* horizontalItem = matchingItems[0];
    NSMenuItem* verticalItem = matchingItems[1];

    NSMenuItem* activeItem =
        horizontalItem.hidden ? verticalItem : horizontalItem;
    NSMenuItem* incomingItem =
        enableVerticalTabs ? verticalItem : horizontalItem;

    if (activeItem != incomingItem) {
      incomingItem.keyEquivalent = activeItem.keyEquivalent;
      incomingItem.keyEquivalentModifierMask =
          activeItem.keyEquivalentModifierMask;

      horizontalItem.hidden = enableVerticalTabs;
      verticalItem.hidden = !enableVerticalTabs;
    }
  };

  // Process "New Tab to the Right" / "New Tab Below" items.
  updateMenuState(IDC_NEW_TAB_TO_RIGHT, enabled);

  // Process "Close Tabs to the Right" / "Close Tabs Below" items.
  updateMenuState(IDC_WINDOW_CLOSE_TABS_TO_RIGHT, enabled);
}

// Called when shutting down or logging out.
- (void)willPowerOff:(NSNotification*)notify {
  // Don't attempt any shutdown here. Cocoa will shortly call
  // -[BrowserCrApplication terminate:].
  _isPoweringOff = YES;
}

- (void)checkForAnyKeyWindows {
  if ([NSApp keyWindow])
    return;

  g_browser_process->platform_part()->key_window_notifier().NotifyNoKeyWindow();
}

// If the auto-update interval is not set, make it 5 hours.
// Placed here for 2 reasons:
// 1) Same spot as other Pref stuff
// 2) Try and be friendly by keeping this after app launch
- (void)setUpdateCheckInterval {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  CFStringRef app = CFSTR("com.google.Keystone.Agent");
  CFStringRef checkInterval = CFSTR("checkInterval");
  base::apple::ScopedCFTypeRef<CFPropertyListRef> plist(
      CFPreferencesCopyAppValue(checkInterval, app));
  if (!plist.get()) {
    const float interval = base::Hours(5).InSecondsF();
    CFPreferencesSetAppValue(
        checkInterval, base::apple::NSToCFPtrCast(@(interval)), app);
    CFPreferencesAppSynchronize(app);
  }
#endif
}

- (void)openStartupUrls {
  CHECK(_startupComplete);
  [self openStartupTabsReplacingNTP:std::move(_startupTabs)];
  _startupTabs.clear();
}

- (void)openStartupTabsReplacingNTP:(StartupTabs)tabs {
  if (tabs.empty()) {
    return;
  }

  if (!_startupComplete) {
    for (auto& tab : tabs) {
      _startupTabs.push_back(std::move(tab));
    }
    return;
  }

  OpenStartupTabsInBrowser(std::move(tabs));
}

- (void)resetKeepAliveWhileHidden {
  NSRunningApplication* app = NSRunningApplication.currentApplication;
  if (_keepAlive &&
      app.activationPolicy == NSApplicationActivationPolicyProhibited) {
    // `_runningApplication` should only be set while `_keepAlive` is not set,
    // as that is the only time we're observing the activationPolicy.
    CHECK(!_runningApplication);

    // Don't keep alive if we're in background/hidden mode. Start observing
    // changes to activationPolicy to re-create `keepAlive_` when chrome stops
    // being hidden.
    _runningApplication = app;
    [_runningApplication addObserver:self
                          forKeyPath:@"activationPolicy"
                             options:NSKeyValueObservingOptionNew
                             context:nullptr];
    _keepAlive.reset();
  }
}

// This is called after profiles have been loaded and preferences registered.
// It is safe to access the default profile here.
- (void)applicationDidFinishLaunching:(NSNotification*)notify {
  if (g_browser_process->browser_policy_connector()
          ->chrome_browser_cloud_management_controller()
          ->IsEnterpriseStartupDialogShowing()) {
    // As Chrome is not ready when the Enterprise startup dialog is being shown.
    // Store the notification as it will be reposted when the dialog is closed.
    return;
  }

  [self setUpdateCheckInterval];

  // If enabled, keep Chrome alive when apps are open instead of quitting all
  // apps.
  _quitWithAppsController = new QuitWithAppsController();

  // Dynamically update shortcuts for "Close Window" and "Close Tab" menu items.
  [self fileMenu].delegate = self;

  // Instantiate the ProfileAttributesStorage observer so that we can get
  // notified when a profile is deleted.
  _profileAttributesStorageObserver =
      std::make_unique<AppControllerProfileObserver>(
          g_browser_process->profile_manager(), self);

  // Record the path to the (browser) app bundle; this is used by the app mode
  // shim.
  if (base::apple::AmIBundled()) {
    base::ThreadPool::PostTask(
        FROM_HERE,
        {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
         base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
        base::BindOnce(&RecordLastRunAppBundlePath));
  }

  // Makes "Services" menu items available.
  [self registerServicesMenuTypesTo:[notify object]];

  _startupComplete = YES;

  if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode))
    ConfigureNSAppForKioskMode();

  BrowserWindowInterface* browser =
      GlobalBrowserCollection::GetInstance()->GetLastActiveBrowser();
  content::WebContents* activeWebContents = nullptr;
  if (browser) {
    activeWebContents = browser->GetTabStripModel()->GetActiveWebContents();
    _lastActiveBrowser = browser->GetWeakPtr();
  }
  [self updateHandoffManager:activeWebContents];
  [self openStartupUrls];

  PrefService* localState = g_browser_process->local_state();
  if (localState) {
    _localPrefRegistrar.Init(localState);
    _localPrefRegistrar.Add(
        prefs::kAllowFileSelectionDialogs,
        base::BindRepeating(
            [](CommandUpdater* commandUpdater) {
              bool enabled = g_browser_process->local_state()->GetBoolean(
                  prefs::kAllowFileSelectionDialogs);
              commandUpdater->UpdateCommandEnabled(IDC_OPEN_FILE, enabled);
            },
            _menuState.get()));
  }

  _handoffObserver = std::make_unique<HandoffObserver>(self);

  ASWebAuthenticationSessionWebBrowserSessionManager.sharedManager
      .sessionHandler = self;

  [MenuControllerCocoa
      initializeWithNewMenuIconScheme:features::IsMenuSimplificationEnabled()];
}

- (void)observeValueForKeyPath:(NSString*)keyPath
                      ofObject:(id)object
                        change:(NSDictionary*)change
                       context:(void*)context {
  if (object != _runningApplication) {
    return;
  }
  // If activationPolicy is no longer set to Prohibited, we can stop observing
  // changes to activationPolicy and recreate `keepAlive_` to keep the browser
  // process alive even without windows present.
  CHECK([keyPath isEqualToString:@"activationPolicy"]);
  if (![change[@"new"] isEqual:@(NSApplicationActivationPolicyProhibited)]) {
    [_runningApplication removeObserver:self
                             forKeyPath:@"activationPolicy"
                                context:nullptr];
    _runningApplication = nil;
    _keepAlive = std::make_unique<ScopedKeepAlive>(
        KeepAliveOrigin::APP_CONTROLLER, KeepAliveRestartOption::DISABLED);
  }
}

// Helper function for populating and displaying the in progress downloads at
// exit alert panel.
- (BOOL)userWillWaitForInProgressDownloads:(int)downloadCount {
  // Set the dialog text based on whether or not there are multiple downloads.
  // Dialog text: warning and explanation.
  NSString* titleText = l10n_util::GetPluralNSStringF(
      IDS_ABANDON_DOWNLOAD_DIALOG_TITLE, downloadCount);
  NSString* explanationText =
      l10n_util::GetNSString(IDS_ABANDON_DOWNLOAD_DIALOG_BROWSER_MESSAGE);

  // "Cancel download and exit" button text.
  NSString* exitTitle =
      l10n_util::GetNSString(IDS_ABANDON_DOWNLOAD_DIALOG_EXIT_BUTTON);

  // "Wait for download" button text.
  NSString* waitTitle =
      l10n_util::GetNSString(IDS_ABANDON_DOWNLOAD_DIALOG_CONTINUE_BUTTON);

  NSAlert* alert = [[NSAlert alloc] init];
  alert.messageText = titleText;
  alert.informativeText = explanationText;
  [alert addButtonWithTitle:waitTitle];
  [alert addButtonWithTitle:exitTitle];

  // 'waitButton' is the default choice.
  int choice = [alert runModal];
  return choice == NSAlertFirstButtonReturn ? YES : NO;
}

// Check all profiles for in progress downloads, and if we find any, prompt the
// user to see if we should continue to exit (and thus cancel the downloads), or
// if we should wait.
- (BOOL)shouldQuitWithInProgressDownloads {
  ProfileManager* profileManager = g_browser_process->profile_manager();
  if (!profileManager) {
    return YES;
  }

  std::vector<Profile*> profiles = profileManager->GetLoadedProfiles();

  // Also consider related OTR profiles.
  std::vector<Profile*> otrProfiles;
  for (const auto& profile : profiles) {
    std::ranges::copy(profile->GetAllOffTheRecordProfiles(),
                      std::back_inserter(otrProfiles));
  }
  std::ranges::copy(otrProfiles, std::back_inserter(profiles));

  // Remove all profiles for which there are no ongoing downloads.
  int totalBlockingDownloadCount = 0;
  auto removed = std::ranges::remove_if(
      profiles, [&totalBlockingDownloadCount](Profile* profile) {
        // If it is not possible to open a browser window for a profile, then
        // don't count that profile towards "downloads in progress".
        if (GetBrowserWindowCreationStatusForProfile(*profile) !=
            Browser::CreationStatus::kOk) {
          return true;
        }

        // `DownloadCoreService` can be nullptr for some irregular profiles,
        // e.g. the System Profile.
        DownloadCoreService* downloadCoreService =
            DownloadCoreServiceFactory::GetForBrowserContext(profile);
        if (!downloadCoreService) {
          return true;
        }

        // Avoid creating a new download manager if one doesn't already exist.
        if (!downloadCoreService->HasCreatedDownloadManager()) {
          return true;
        }

        int blockingDownloads =
            profile->GetDownloadManager()->BlockingShutdownCount();
        totalBlockingDownloadCount += blockingDownloads;
        return blockingDownloads == 0;
      });
  profiles.erase(removed.begin(), removed.end());

  if (profiles.empty()) {
    // No profiles with active downloads were found, so it is okay to exit.
    return YES;
  }

  // A dialog is about to be shown to the user, and that involves running a
  // nested run loop, during which it is possible that Profiles may disappear.
  // Therefore, from this point forward, be sure to refer to Profile objects via
  // weak pointers. See https://crbug.com/507386203.
  std::vector<base::WeakPtr<Profile>> weakProfiles;
  weakProfiles.reserve(profiles.size());
  std::ranges::transform(
      profiles, std::back_inserter(weakProfiles),
      [](Profile* profile) { return profile->GetWeakPtr(); });

  // Pop the question, and return if the user chooses to quit.
  if (![self userWillWaitForInProgressDownloads:totalBlockingDownloadCount]) {
    return YES;
  }

  // Display the active downloads, creating new browser windows if necessary.
  for (auto& profile : weakProfiles) {
    if (!profile.get()) {
      continue;
    }

    BrowserWindowInterface* browser =
        ProfileBrowserCollection::GetForProfile(profile.get())
            ->GetLastActiveBrowser();
    if (!browser) {
      browser = CreateBrowserWindow(
          BrowserWindowCreateParams(profile.get(), /*user_gesture=*/true));
      browser->GetWindow()->Show();
    }

    chrome::ShowDownloads(browser);
  }

  [self cancelConfirmQuitPanel];
  return NO;
}

// Called to determine if we should enable the "restore tab" menu item.
// Checks with the TabRestoreService to see if there's anything there to
// restore and returns YES if so.
//
// In the zero-profile state, use the value from when the last profile was
// still loaded (if ever).
- (BOOL)canRestoreTab {
  Profile* lastProfile = [self lastProfileIfLoaded];
  if (!lastProfile)
    return _tabRestoreWasEnabled;
  sessions::TabRestoreService* service =
      TabRestoreServiceFactory::GetForProfile(lastProfile);
  return service && !service->entries().empty();
}

// Called from the AppControllerProfileObserver every time a profile is deleted.
- (void)profileWasRemoved:(const base::FilePath&)profilePath
             forIncognito:(bool)isOffTheRecord {
  // If the lastProfile has been deleted, the profile manager has
  // already loaded a new one, so the pointer needs to be updated;
  // otherwise we will try to start up a browser window with a pointer
  // to the old profile.
  //
  // In a browser test, the application is not brought to the front, so
  // |_lastProfile| might be null.
  if (!_lastProfile || (profilePath == _lastProfile->GetPath() &&
                        isOffTheRecord == _lastProfile->IsOffTheRecord())) {
    Profile* last_used_profile = nullptr;
    auto* profile_manager = g_browser_process->profile_manager();
    if (profile_manager) {
      // |profile_manager| is null in browser tests during shutdown.
      last_used_profile = profile_manager->GetLastUsedProfileIfLoaded();
    }
    [self setLastProfile:last_used_profile];
  }

  auto it = _profileBookmarkMenuBridgeMap.find(profilePath);
  if (it != _profileBookmarkMenuBridgeMap.end() &&
      (!base::FeatureList::IsEnabled(features::kDestroyProfileOnBrowserClose) ||
       (it->second->GetProfile() && !isOffTheRecord))) {
    // Clean up the dangling Profile* in |_profileBookmarkMenuBridgeMap|.
    //
    // No need to clean up when |isOffTheRecord|, because BookmarkMenuBridge
    // always points to a non-OTR profile.
    _profileBookmarkMenuBridgeMap.erase(it);
  }
}

// Returns true if there is a modal window (either window- or application-
// modal) blocking the active browser. Note that tab modal dialogs (HTTP auth
// sheets) will not count as blocking the browser. But things like open/save
// dialogs that are window modal will block the browser.
- (BOOL)keyWindowIsModal {
  if ([NSApp modalWindow])
    return YES;

  BrowserWindowInterface* browser =
      GlobalBrowserCollection::GetInstance()->GetLastActiveBrowser();
  return browser &&
         [[browser->GetWindow()->GetNativeWindow().GetNativeNSWindow()
             attachedSheet] isKindOfClass:[NSWindow class]];
}

- (BOOL)canOpenNewBrowser {
  Profile* unsafeLastProfile = [self lastProfileIfLoaded];
  // If the profile is not loaded, try to load it. If it's not usable, the
  // profile picker will be open instead.
  if (!unsafeLastProfile)
    return YES;
  return [self safeProfileForNewWindows:unsafeLastProfile] ? YES : NO;
}

// Validates menu items in the dock (always) and in the menu bar (if there is no
// browser).
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
  SEL action = [item action];
  BOOL enable = NO;
  // Whether the profile is loaded and opening a new browser window is allowed.
  BOOL canOpenNewBrowser = [self canOpenNewBrowser];
  BOOL hasLoadedProfile = [self lastProfileIfLoaded] ? YES : NO;
  // Commands from dock are always handled by commandFromDock:, but commands
  // from the menu bar are only handled by commandDispatch: if there is no key
  // window.
  if (action == @selector(commandDispatch:) ||
      action == @selector(commandFromDock:)) {
    NSInteger tag = [item tag];
    if (_menuState &&  // NULL in tests.
        _menuState->SupportsCommand(tag)) {
      switch (tag) {
        // The File Menu commands are not automatically disabled by Cocoa when a
        // dialog sheet obscures the browser window, so we disable several of
        // them here.  We don't need to include IDC_CLOSE_WINDOW, because
        // app_controller is only activated when there are no key windows (see
        // function comment).
        case IDC_RESTORE_TAB:
          enable = ![self keyWindowIsModal] && [self canRestoreTab];
          break;
        // Profile-level items that affect how the profile's UI looks should
        // only be available while there is a Profile opened.
        case IDC_SHOW_FULL_URLS:
        case IDC_SHOW_AI_MODE_OMNIBOX_BUTTON:
        case IDC_SHOW_GOOGLE_LENS_SHORTCUT:
        case IDC_SHOW_SEARCH_TOOLS:
          enable = hasLoadedProfile;
          break;
        // Browser-level items that open in new tabs or perform an action in a
        // current tab should not open if there's a window- or app-modal dialog.
        case IDC_OPEN_FILE:
        case IDC_NEW_TAB:
        case IDC_FOCUS_LOCATION:
        case IDC_FOCUS_SEARCH:
        case IDC_SHOW_HISTORY:
        case IDC_SHOW_BOOKMARK_MANAGER:
        case IDC_CLEAR_BROWSING_DATA:
        case IDC_SHOW_DOWNLOADS:
        case IDC_IMPORT_SETTINGS:
        case IDC_MANAGE_EXTENSIONS:
        case IDC_HELP_PAGE_VIA_MENU:
        case IDC_OPTIONS:
          enable = canOpenNewBrowser && ![self keyWindowIsModal];
          break;
        // Browser-level items that open in new windows: allow the user to open
        // a new window even if there's a window-modal dialog.
        case IDC_NEW_WINDOW:
          enable = canOpenNewBrowser;
          break;
        case IDC_TASK_MANAGER_MAIN_MENU:
        case IDC_EXIT:
          enable = YES;
          break;
        case IDC_NEW_INCOGNITO_WINDOW:
        case IDC_NEW_ISOLATED_WINDOW:
          enable = _menuState->IsCommandEnabled(tag) ? canOpenNewBrowser : NO;
          break;
        default:
          enable = _menuState->IsCommandEnabled(tag) ?
                   ![self keyWindowIsModal] : NO;
          break;
      }
    }

    // "Show as tab" should only appear when the current window is a popup.
    // Since |validateUserInterfaceItem:| is called only when there are no
    // key windows, we should just hide this.
    // This is handled outside of the switch statement because we want to hide
    // this regardless if the command is supported or not.
    if (tag == IDC_SHOW_AS_TAB) {
      NSMenuItem* menuItem = base::apple::ObjCCast<NSMenuItem>(item);
      [menuItem setHidden:YES];
    }
  } else if (action == @selector(terminate:)) {
    enable = YES;
  } else if (action == @selector(showPreferences:)) {
    enable = canOpenNewBrowser;
  } else if (action == @selector(orderFrontStandardAboutPanel:)) {
    enable = canOpenNewBrowser;
  } else if (action == @selector(toggleConfirmToQuit:)) {
    [self updateConfirmToQuitPrefMenuItem:static_cast<NSMenuItem*>(item)];
    enable = [self shouldEnableConfirmToQuitPrefMenuItem];
  }
  return enable;
}

- (void)commandDispatch:(id)sender {
  // Drop commands received after shutdown was initiated.
  if (g_browser_process->IsShuttingDown() ||
      browser_shutdown::IsTryingToQuit()) {
    return;
  }

  // Handle the case where we're dispatching a command from a sender that's in a
  // browser window. This means that the command came from a background window
  // and is getting here because the foreground window is not a browser window.
  if ([sender respondsToSelector:@selector(window)]) {
    id delegate = [[sender window] windowController];
    if ([delegate respondsToSelector:@selector(commandDispatch:)]) {
      [delegate commandDispatch:sender];
      return;
    }
  }

  // If not between -applicationDidFinishLaunching: and
  // -applicationWillTerminate:, ignore. This can happen when events are sitting
  // in the event queue while the browser is shutting down.
  if (!_keepAlive) {
    return;
  }

  Profile* unsafeLastProfile = [self lastProfileIfLoaded];
  Profile* lastProfile = [self safeProfileForNewWindows:unsafeLastProfile];
  // Ignore commands during session restore's browser creation.  It uses a
  // nested run loop and commands dispatched during this operation cause
  // havoc.
  if (lastProfile && SessionRestore::IsRestoring(lastProfile) &&
      base::RunLoop::IsNestedOnCurrentThread()) {
    return;
  }

  NSInteger tag = [sender tag];

  if (tag == IDC_EXIT) {
    chrome::AttemptUserExit();
    return;
  }

  // The task manager can be shown without profile.
  if (tag == IDC_TASK_MANAGER_MAIN_MENU) {
    chrome::OpenTaskManager(nullptr, task_manager::StartAction::kMainMenu);
    return;
  }

  if (unsafeLastProfile && !lastProfile) {
    // The profile is disallowed by policy (locked or guest mode disabled).
    ProfilePicker::Show(ProfilePicker::Params::FromEntryPoint(
        ProfilePicker::EntryPoint::kProfileLocked));
    return;
  }

  // Asynchronously load profile first if needed.
  app_controller_mac::RunInLastProfileSafely(
      base::BindOnce(^(Profile* profile) {
        [self executeCommand:sender withProfile:profile];
      }),
      app_controller_mac::kShowProfilePickerOnFailure);
}

- (void)executeCommand:(id)sender withProfile:(Profile*)profile {
  if (!profile) {
    // Couldn't load the Profile. RunInSafeProfileHelper will show the
    // ProfilePicker instead.
    return;
  }

  NSInteger tag = [sender tag];

  switch (tag) {
    case IDC_NEW_TAB:
      // Create a new tab in an existing browser window (which we activate) if
      // possible.
      if (BrowserWindowInterface* browser = ActivateBrowser(profile)) {
        chrome::ExecuteCommand(browser, IDC_NEW_TAB);
        break;
      }
      [[fallthrough]];  // To create new window.
    case IDC_NEW_WINDOW:
      CreateBrowser(profile->GetOriginalProfile());
      break;
    case IDC_FOCUS_LOCATION:
      chrome::ExecuteCommand(ActivateOrCreateBrowser(profile),
                             IDC_FOCUS_LOCATION);
      break;
    case IDC_FOCUS_SEARCH:
      chrome::ExecuteCommand(ActivateOrCreateBrowser(profile),
                             IDC_FOCUS_SEARCH);
      break;
    case IDC_NEW_INCOGNITO_WINDOW:
    case IDC_NEW_ISOLATED_WINDOW:
      CreateBrowser(profile->GetPrimaryOTRProfile(/*create_if_needed=*/true));
      break;
    case IDC_RESTORE_TAB:
      app_controller_mac::TabRestorer::RestoreMostRecent(profile);
      break;
    case IDC_OPEN_FILE:
      chrome::ExecuteCommand(CreateBrowser(profile), IDC_OPEN_FILE);
      break;
    case IDC_CLEAR_BROWSING_DATA: {
      // There may not be a browser open, so use the default profile.
      if (BrowserWindowInterface* browser = ActivateBrowser(profile)) {
        chrome::ShowClearBrowsingDataDialog(browser);
      } else {
        chrome::OpenClearBrowsingDataDialogWindow(profile);
      }
      break;
    }
    case IDC_IMPORT_SETTINGS: {
      if (BrowserWindowInterface* browser = ActivateBrowser(profile)) {
        chrome::ShowImportDialog(browser);
      } else {
        chrome::OpenImportSettingsDialogWindow(profile);
      }
      break;
    }
    case IDC_SHOW_BOOKMARK_MANAGER:
      if (BrowserWindowInterface* browser = ActivateBrowser(profile)) {
        chrome::ShowBookmarkManager(browser);
      } else {
        // No browser window, so create one for the bookmark manager tab.
        chrome::OpenBookmarkManagerWindow(profile);
      }
      break;
    case IDC_SHOW_HISTORY:
      if (BrowserWindowInterface* browser = ActivateBrowser(profile)) {
        chrome::ShowHistory(browser);
      } else {
        chrome::OpenHistoryWindow(profile);
      }
      break;
    case IDC_SHOW_DOWNLOADS:
      if (BrowserWindowInterface* browser = ActivateBrowser(profile)) {
        chrome::ShowDownloads(browser);
      } else {
        chrome::OpenDownloadsWindow(profile);
      }
      break;
    case IDC_MANAGE_EXTENSIONS:
      if (BrowserWindowInterface* browser = ActivateBrowser(profile)) {
        chrome::ShowExtensions(browser);
      } else {
        chrome::OpenExtensionsWindow(profile);
      }
      break;
    case IDC_HELP_PAGE_VIA_MENU:
      if (BrowserWindowInterface* browser = ActivateBrowser(profile)) {
        chrome::ShowHelp(browser, chrome::HelpSource::kMenu);
      } else {
        chrome::OpenHelpWindow(profile, chrome::HelpSource::kMenu);
      }
      break;
    case IDC_OPTIONS:
      [self showPreferences:sender];
      break;
  }
}

// Same as |-commandDispatch:|, but executes commands using a disposition
// determined by the key flags. This will get called in the case where the
// frontmost window is not a browser window, and the user has command-clicked
// a button in a background browser window whose action is
// |-commandDispatchUsingKeyModifiers:|
- (void)commandDispatchUsingKeyModifiers:(id)sender {
  DCHECK(sender);
  if ([sender respondsToSelector:@selector(window)]) {
    id delegate = [[sender window] windowController];
    if ([delegate respondsToSelector:
            @selector(commandDispatchUsingKeyModifiers:)]) {
      [delegate commandDispatchUsingKeyModifiers:sender];
    }
  }
}

// NSApplication delegate method called when someone clicks on the dock icon.
// To match standard mac behavior, we should open a new window if there are no
// browser windows.
- (BOOL)applicationShouldHandleReopen:(NSApplication*)theApplication
                    hasVisibleWindows:(BOOL)hasVisibleWindows {
  // If the browser is currently trying to quit, don't do anything and return NO
  // to prevent AppKit from doing anything.
  if (browser_shutdown::IsTryingToQuit())
    return NO;

  // Bring all browser windows to the front. Specifically, this brings them in
  // front of any app windows. FocusWindowSet will also unminimize the most
  // recently minimized window if no windows in the set are visible.
  // If there are any, return here. Otherwise, the windows are panels or
  // notifications so we still need to open a new window.
  if (hasVisibleWindows) {
    NSSet<NSWindow*>* browserWindows = GetBrowserWindows();
    if (browserWindows.count) {
      FocusWindowSetOnCurrentSpace(browserWindows);
      // We've performed the unminimize, so AppKit shouldn't do anything.
      return NO;
    }

    if (ProfilePicker::IsOpen()) {
      ProfilePicker::Show(ProfilePicker::Params::FromEntryPoint(
          // The entry point should be irrelevant here because the picker is
          // already open.
          ProfilePicker::EntryPoint::kNewSessionOnExistingProcess));
      return NO;
    }
  }

  base::FilePath lastProfilePath = GetStartupProfilePathMac();
  DCHECK_NE(lastProfilePath, ProfileManager::GetSystemProfilePath());

  // If launched as a hidden login item (due to installation of a persistent app
  // or by the user, for example in System Settings->General->Login Items),
  // allow the session to be restored first time the user clicks on a Dock icon.
  // Normally, it'd just open a new empty page.
  static BOOL doneOnce = NO;
  BOOL attemptRestore =
      apps::AppShimTerminationManager::Get()->ShouldRestoreSession() ||
      (!doneOnce && base::mac::WasLaunchedAsHiddenLoginItem());
  doneOnce = YES;

  // If the profile is locked or was off-the-record, open the profile picker.
  if (lastProfilePath == ProfileManager::GetGuestProfilePath() ||
      IsProfileSignedOut(lastProfilePath)) {
    ProfilePicker::Show(ProfilePicker::Params::FromEntryPoint(
        ProfilePicker::EntryPoint::kProfileLocked));
    return NO;
  }

  if (attemptRestore) {
    // Load the profile and attempt session restore.
    app_controller_mac::RunInLastProfileSafely(
        base::BindOnce(&AttemptSessionRestore),
        app_controller_mac::kShowProfilePickerOnFailure);
    return NO;
  }

  // Open the profile picker (for multi-profile users) or a new window.
  if (ProfilePicker::GetStartupMode() == StartupProfileMode::kProfilePicker) {
    ProfilePicker::Show(ProfilePicker::Params::FromEntryPoint(
        ProfilePicker::EntryPoint::kNewSessionOnExistingProcess));
  } else {
    // Asynchronously load profile first if needed.
    // TODO(crbug.com/40261514): Replace CreateBrowser by LaunchBrowserStartup
    app_controller_mac::RunInLastProfileSafely(
        base::BindOnce([](Profile* profile) {
          if (!profile) {
            return;
          }
          CreateBrowser(profile);
        }),
        app_controller_mac::kShowProfilePickerOnFailure);
  }

  // We've handled the reopen event, so return NO to tell AppKit not
  // to do anything.
  return NO;
}

- (void)initMenuState {
  _menuState = std::make_unique<CommandUpdaterImpl>(nullptr);
  _menuState->UpdateCommandEnabled(IDC_NEW_TAB, true);
  _menuState->UpdateCommandEnabled(IDC_NEW_WINDOW, true);
  _menuState->UpdateCommandEnabled(IDC_NEW_INCOGNITO_WINDOW, true);
  _menuState->UpdateCommandEnabled(IDC_NEW_ISOLATED_WINDOW, true);
  _menuState->UpdateCommandEnabled(IDC_OPEN_FILE, true);
  _menuState->UpdateCommandEnabled(IDC_CLEAR_BROWSING_DATA, true);
  _menuState->UpdateCommandEnabled(IDC_RESTORE_TAB, false);
  _menuState->UpdateCommandEnabled(IDC_FOCUS_LOCATION, true);
  _menuState->UpdateCommandEnabled(IDC_FOCUS_SEARCH, true);
  _menuState->UpdateCommandEnabled(IDC_SHOW_BOOKMARK_MANAGER, true);
  _menuState->UpdateCommandEnabled(IDC_SHOW_HISTORY, true);
  _menuState->UpdateCommandEnabled(IDC_SHOW_DOWNLOADS, true);
  _menuState->UpdateCommandEnabled(IDC_MANAGE_EXTENSIONS, true);
  _menuState->UpdateCommandEnabled(IDC_HELP_PAGE_VIA_MENU, true);
  _menuState->UpdateCommandEnabled(IDC_IMPORT_SETTINGS, true);
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  _menuState->UpdateCommandEnabled(IDC_FEEDBACK, true);
#endif
  _menuState->UpdateCommandEnabled(IDC_TASK_MANAGER_MAIN_MENU, true);
  _menuState->UpdateCommandEnabled(IDC_EXIT, true);
}

// Conditionally adds the Profile menu to the main menu bar.
- (void)initProfileMenu {
  NSMenu* mainMenu = [NSApp mainMenu];
  NSMenuItem* profileMenu = [mainMenu itemWithTag:IDC_PROFILE_MAIN_MENU];

  if (!profiles::IsMultipleProfilesEnabled()) {
    [mainMenu removeItem:profileMenu];
    return;
  }

  // The controller will unhide the menu if necessary.
  [profileMenu setHidden:YES];

  _profileMenuController =
      [[ProfileMenuController alloc] initWithMainMenuItem:profileMenu];
}

- (void)initShareMenu {
  _shareMenuController = [[ShareMenuController alloc] init];
  NSMenu* fileMenu = [self fileMenu];
  NSString* shareMenuTitle = l10n_util::GetNSString(IDS_SHARE_MAC);
  NSMenuItem* shareMenuItem = [fileMenu itemWithTitle:shareMenuTitle];
  NSMenu* shareSubmenu = [[NSMenu alloc] initWithTitle:shareMenuTitle];
  shareSubmenu.delegate = _shareMenuController;
  shareMenuItem.submenu = shareSubmenu;
}

// The Confirm to Quit preference is atypical in that the preference lives in
// the app menu right above the Quit menu item. This method will refresh the
// display of that item depending on the preference state.
- (void)updateConfirmToQuitPrefMenuItem:(NSMenuItem*)item {
  // Format the string so that the correct key equivalent is displayed.
  NSString* acceleratorString = [ConfirmQuitPanelController keyCommandString];
  NSString* title = l10n_util::GetNSStringF(IDS_CONFIRM_TO_QUIT_OPTION,
      base::SysNSStringToUTF16(acceleratorString));
  [item setTitle:title];

  const PrefService* prefService = g_browser_process->local_state();
  bool enabled = prefService->GetBoolean(prefs::kConfirmToQuitEnabled);
  [item setState:enabled ? NSControlStateValueOn : NSControlStateValueOff];
}

- (BOOL)shouldEnableConfirmToQuitPrefMenuItem {
  const PrefService* prefService = g_browser_process->local_state();
  return !prefService->FindPreference(prefs::kConfirmToQuitEnabled)
              ->IsManaged();
}

- (void)registerServicesMenuTypesTo:(NSApplication*)app {
  // Note that RenderWidgetHostViewCocoa implements NSServicesRequests which
  // handles requests from services.
  [app registerServicesMenuSendTypes:@[ NSPasteboardTypeString ]
                         returnTypes:@[ NSPasteboardTypeString ]];
}

// Returns null if the profile is not loaded in memory.
- (Profile*)lastProfileIfLoaded {
  // Return the profile of the last-used Browser, if available.
  if (_lastProfile)
    return _lastProfile;

  if (![self isProfileReady])
    return nullptr;

  ProfileManager* profile_manager = g_browser_process->profile_manager();
  if (!profile_manager)
    return nullptr;

  // GetProfileByPath() returns nullptr if the profile is not loaded.
  return profile_manager->GetProfileByPath(GetStartupProfilePathMac());
}

// Returns null if Chrome is not ready or there is no ProfileManager.
// DEPRECATED: use lastProfileIfLoaded instead.
// TODO(crbug.com/40054768): May be blocking, migrate all callers to
// |-lastProfileIfLoaded|.
- (Profile*)lastProfile {
  Profile* lastLoadedProfile = [self lastProfileIfLoaded];
  if (lastLoadedProfile)
    return lastLoadedProfile;

  if (![self isProfileReady])
    return nullptr;

  return GetLastProfileMac();
}

- (Profile*)safeProfileForNewWindows:(Profile*)profile {
  if (!profile)
    return nullptr;

  DCHECK(!profile->IsSystemProfile());
  if (profile->IsGuestSession() && !profiles::IsGuestModeEnabled())
    return nullptr;

  if (IsProfileSignedOut(profile->GetPath()))
    return nullptr;  // Profile is locked.

  return ProfileManager::MaybeForceOffTheRecordMode(profile);
}

- (void)application:(NSApplication*)sender openURLs:(NSArray<NSURL*>*)urls {
  StartupTabs startupTabs;
  for (NSURL* url in urls) {
    // Handle the google-chrome:// scheme (and chromium://).
    // We convert every URL to string here to reuse the shared
    // StripGoogleChromeScheme logic. While we could check [url scheme] first
    // for efficiency, this path is user-initiated and low-frequency, so sharing
    // the stripping logic is preferred.
    std::string urlString = base::SysNSStringToUTF8([url absoluteString]);
    base::FilePath::StringViewType urlStringView = urlString;
    if (startup::StripGoogleChromeScheme(urlStringView)) {
      GURL gurl(urlStringView);
      if (startup::ValidateLaunchUrlWebSafe(gurl)) {
        startupTabs.emplace_back(gurl, /*is_untrusted_launch=*/true);
      }
    } else {
      startupTabs.emplace_back(net::GURLWithNSURL(url),
                               /*is_untrusted_launch=*/false);
    }
  }

  if (!startupTabs.empty()) {
    [self openStartupTabsReplacingNTP:std::move(startupTabs)];
  }
}

// Show the preferences window, or bring it to the front if it's already
// visible.
- (IBAction)showPreferences:(id)sender {
  // Asynchronously load profile first if needed.
  app_controller_mac::RunInLastProfileSafely(
      base::BindOnce(^(Profile* profile) {
        [self showPreferencesForProfile:profile];
      }),
      app_controller_mac::kShowProfilePickerOnFailure);
}

- (IBAction)showPreferencesForProfile:(Profile*)profile {
  if (!profile) {
    // Failed to load profile, show Profile Picker instead.
    return;
  }
  // Re-use an existing browser, or create a new one.
  if (BrowserWindowInterface* browser = ActivateBrowser(profile)) {
    chrome::ShowSettings(browser);
  } else {
    chrome::OpenOptionsWindow(profile);
  }
}

- (IBAction)orderFrontStandardAboutPanel:(id)sender {
  // Asynchronously load profile first if needed.
  app_controller_mac::RunInLastProfileSafely(
      base::BindOnce(^(Profile* profile) {
        [self orderFrontStandardAboutPanelForProfile:profile];
      }),
      app_controller_mac::kShowProfilePickerOnFailure);
}

- (IBAction)orderFrontStandardAboutPanelForProfile:(Profile*)profile {
  if (!profile) {
    // Failed to load profile, show Profile Picker instead.
    return;
  }
  // Re-use an existing browser, or create a new one.
  if (BrowserWindowInterface* browser = ActivateBrowser(profile)) {
    chrome::ShowAboutChrome(browser);
  } else {
    chrome::OpenAboutWindow(profile);
  }
}

- (IBAction)toggleConfirmToQuit:(id)sender {
  PrefService* prefService = g_browser_process->local_state();
  bool enabled = prefService->GetBoolean(prefs::kConfirmToQuitEnabled);
  prefService->SetBoolean(prefs::kConfirmToQuitEnabled, !enabled);
}

// Explicitly bring to the foreground when creating new windows from the dock.
- (void)commandFromDock:(id)sender {
  [NSApp activateIgnoringOtherApps:YES];
  [self commandDispatch:sender];
}

- (NSMenu*)applicationDockMenu:(NSApplication*)sender {
  NSMenu* dockMenu = [[NSMenu alloc] initWithTitle:@""];

  BOOL profilesAdded = [_profileMenuController insertItemsIntoMenu:dockMenu
                                                          atOffset:0
                                                          fromDock:YES];
  if (profilesAdded) {
    [dockMenu addItem:[NSMenuItem separatorItem]];
  }

  NSString* titleStr = l10n_util::GetNSStringWithFixup(IDS_NEW_WINDOW_MAC);
  NSMenuItem* item =
      [[NSMenuItem alloc] initWithTitle:titleStr
                                 action:@selector(commandFromDock:)
                          keyEquivalent:@""];
  item.target = self;
  item.tag = IDC_NEW_WINDOW;
  item.enabled = [self validateUserInterfaceItem:item];
  [dockMenu addItem:item];

  Profile* profile = [self lastProfileIfLoaded];

  // Buttons below require the profile to be loaded. In particular, if the
  // profile picker is shown at startup, these buttons won't be added until the
  // user picks a profile.
  if (!profile) {
    return dockMenu;
  }

  bool isolated_mode_enabled =
      enterprise_isolated_mode::IsolatedModeReplacesIncognito(profile);

  if (IncognitoModePrefs::GetAvailability(profile->GetPrefs()) !=
      policy::IncognitoModeAvailability::kDisabled) {
    titleStr = l10n_util::GetNSStringWithFixup(IDS_NEW_INCOGNITO_WINDOW_MAC);
    item = [[NSMenuItem alloc] initWithTitle:titleStr
                                      action:@selector(commandFromDock:)
                               keyEquivalent:@""];
    item.target = self;
    item.tag = IDC_NEW_INCOGNITO_WINDOW;
    item.enabled = [self validateUserInterfaceItem:item];
    [dockMenu addItem:item];
  }

  if (isolated_mode_enabled) {
    titleStr = l10n_util::GetNSStringWithFixup(IDS_NEW_ISOLATED_WINDOW_MAC);
    item = [[NSMenuItem alloc] initWithTitle:titleStr
                                      action:@selector(commandFromDock:)
                               keyEquivalent:@""];
    item.target = self;
    item.tag = IDC_NEW_ISOLATED_WINDOW;
    item.enabled = [self validateUserInterfaceItem:item];
    [dockMenu addItem:item];
  }

  return dockMenu;
}


- (BookmarkMenuBridge*)bookmarkMenuBridge {
  return _bookmarkMenuBridge;
}

- (HistoryMenuBridge*)historyMenuBridge {
  return _historyMenuBridge.get();
}

- (TabMenuBridge*)tabMenuBridge {
  return _tabMenuBridge.get();
}

- (void)setLastProfile:(Profile*)profile {
  if (profile == _lastProfile)
    return;

  // If _lastProfile becomes null, remember the last state of Cmd+Shift+T so the
  // command can continue working (or stay disabled). This is primarily meant
  // for the zero-profile state.
  _tabRestoreWasEnabled = profile == nullptr && [self canRestoreTab];

  // Before tearing down the menu controller bridges, return the history menu to
  // its initial state.
  if (profile != nullptr) {
    if (_historyMenuBridge)
      _historyMenuBridge->ResetMenu();
    _historyMenuBridge.reset();
  } else if (_historyMenuBridge && !_isShuttingDown) {
    _historyMenuBridge->OnProfileWillBeDestroyed();
  }


  _profilePrefRegistrar.reset();

  // Update Incognito and Isolated Mode menu items based on enterprise policy.
  // Isolated Mode replaces standard Incognito for enterprise users, but they
  // offer different privacy guarantees. To highlight this distinction, we
  // keep the Incognito item visible, but disabled, rather than hiding it,
  // making it clear that Isolated Mode is active instead.
  NSMenuItem* fileMenuItem = [NSApp.mainMenu itemWithTag:IDC_FILE_MENU];
  if (fileMenuItem && fileMenuItem.hasSubmenu) {
    NSMenu* fileMenu = fileMenuItem.submenu;
    NSMenuItem* incognitoItem = [fileMenu itemWithTag:IDC_NEW_INCOGNITO_WINDOW];
    NSMenuItem* isolatedItem = [fileMenu itemWithTag:IDC_NEW_ISOLATED_WINDOW];

    if (incognitoItem && isolatedItem) {
      bool isolated_mode_enabled =
          enterprise_isolated_mode::IsolatedModeReplacesIncognito(profile);

      // Toggle visibility of Isolated Mode item based on policy.
      isolatedItem.hidden = !isolated_mode_enabled;

      // Both modes logically share the same keyboard shortcut (Cmd+Shift+N) and
      // the same underlying function to open the window (which opens a browser
      // with the primary OTR profile, behaving differently based on policy).
      // To avoid confusion, assign the shortcut to the active/enabled item
      // and clear it from the other.
      NSMenuItem* targetItem =
          isolated_mode_enabled ? isolatedItem : incognitoItem;
      NSMenuItem* sourceItem =
          isolated_mode_enabled ? incognitoItem : isolatedItem;

      if (sourceItem.keyEquivalent.length > 0) {
        targetItem.keyEquivalent = sourceItem.keyEquivalent;
        targetItem.keyEquivalentModifierMask =
            sourceItem.keyEquivalentModifierMask;

        sourceItem.keyEquivalent = @"";
        sourceItem.keyEquivalentModifierMask = 0;
      }
    }
  }

  NSMenuItem* bookmarkItem = [NSApp.mainMenu itemWithTag:IDC_BOOKMARKS_MENU];
  BOOL hidden = bookmarkItem.hidden;
  if (profile != nullptr) {
    // Rebuild the menus with the new profile. The bookmarks submenu is cached
    // to avoid slowdowns when switching between profiles with large numbers of
    // bookmarks. Before caching, store whether it is hidden, make the menu item
    // visible, and restore its original hidden state after resetting the
    // submenu. This works around an apparent AppKit bug where setting a
    // *different* NSMenu submenu on a *hidden* menu item forces the item to
    // become visible. See https://crbug.com/40421657 for more details.
    bookmarkItem.hidden = NO;
    _bookmarkMenuBridge = nullptr;
  } else if (_bookmarkMenuBridge && !_isShuttingDown) {
    DCHECK_EQ(_bookmarkMenuBridge->GetProfile(),
              _lastProfile->GetOriginalProfile());
    // |_bookmarkMenuBridge| always points to the original profile. So, no need
    // to call OnProfileWillBeDestroyed() when the OTR profile is destroyed.
    if (!_lastProfile->IsOffTheRecord()) {
      _bookmarkMenuBridge->OnProfileWillBeDestroyed();
    }
  }

  _lastProfile = profile;

  if (_lastProfile == nullptr)
    return;

  auto& entry = _profileBookmarkMenuBridgeMap[profile->GetPath()];
  if (!entry || !entry->GetProfile()) {
    // This creates a deep copy, but only the first 3 items in the root menu
    // are really wanted. This can probably be optimized, but lazy-loading of
    // the menu should reduce the impact in most flows.
    NSMenu* submenu = [bookmarkItem.submenu copy];
    submenu.delegate = nil;  // The delegate is also copied. Remove it.

    // The original profile outlives the OTR profile. Always create the bridge
    // on the original profile, to prevent bugs WRT profile lifetime.
    entry = std::make_unique<BookmarkMenuBridge>(profile->GetOriginalProfile(),
                                                 submenu);

    // Clear bookmarks from the old profile.
    entry->ClearBookmarkMenu();
  }
  _bookmarkMenuBridge = entry.get();

  // No need to |BuildMenu| here.  It is done lazily upon menu access.
  bookmarkItem.submenu = _bookmarkMenuBridge->BookmarkMenu();
  bookmarkItem.hidden = hidden;

  _historyMenuBridge = std::make_unique<HistoryMenuBridge>(_lastProfile);
  _historyMenuBridge->BuildMenu();


  chrome::BrowserCommandController::
      UpdateSharedCommandsForIncognitoAvailability(
          _menuState.get(), _lastProfile);
  _profilePrefRegistrar = std::make_unique<PrefChangeRegistrar>();
  _profilePrefRegistrar->Init(_lastProfile->GetPrefs());
  _profilePrefRegistrar->Add(
      policy::policy_prefs::kIncognitoModeAvailability,
      base::BindRepeating(&chrome::BrowserCommandController::
                              UpdateSharedCommandsForIncognitoAvailability,
                          _menuState.get(), _lastProfile));
}

- (const ui::ColorProvider&)lastActiveColorProvider {
  BrowserWindowInterface* browser = _lastActiveBrowser.get();
  if (browser) {
    auto* helper = ColorProviderBrowserHelper::From(browser);
    if (helper) {
      const auto* color_provider =
          helper->color_provider_source()->GetColorProvider();
      if (color_provider) {
        return *color_provider;
      }
    }
  }
  return *ui::ColorProviderManager::Get().GetColorProviderFor(
      ui::NativeTheme::GetInstanceForNativeUi()->GetColorProviderKey(nullptr));
}

- (id)targetForPerformClose {
  return _mainWindowForTesting
             ? _mainWindowForTesting
             : [NSApp targetForAction:@selector(performClose:)];
}

// Returns the NSWindow that's the target of the Close Window command.
- (NSWindow*)windowForPerformClose {
  NSWindow* targetWindow = nil;
  id target = [self targetForPerformClose];

  // If `target` is a popover (likely the dictionary lookup popover), the
  // main window should handle the close menu item action.
  if ([target isKindOfClass:[NSPopover class]]) {
    targetWindow =
        [[[base::apple::ObjCCast<NSPopover>(target) contentViewController] view]
            window];
  } else {
    targetWindow = base::apple::ObjCCast<NSWindow>(target);
  }

  // If `targetWindow` is a child (a popover or bubble), the topmost parent
  // window should handle the command.
  while (targetWindow.parentWindow) {
    targetWindow = targetWindow.parentWindow;
  }

  return targetWindow;
}

- (BOOL)windowHasBrowserTabs:(NSWindow*)window {
  if (!window) {
    return NO;
  }
  BrowserWindowInterface* browser =
      GlobalBrowserCollection::GetInstance()->FindBrowserWithWindow(
          gfx::NativeWindow(window));

  return browser &&
         browser->GetType() == BrowserWindowInterface::Type::TYPE_NORMAL &&
         !browser->GetTabStripModel()->empty();
}

- (void)configureMenuItemForCloseTab:(NSMenuItem*)menuItem {
  menuItem.title = l10n_util::GetNSStringWithFixup(IDS_CLOSE_TAB_MAC);
  menuItem.hidden = NO;
  menuItem.tag = IDC_CLOSE_TAB;
  menuItem.action = @selector(commandDispatch:);
}

- (void)configureMenuItemForCloseWindow:(NSMenuItem*)menuItem {
  menuItem.title = l10n_util::GetNSStringWithFixup(IDS_CLOSE_WINDOW_MAC);
  menuItem.hidden = NO;
  menuItem.tag = IDC_CLOSE_WINDOW;
  menuItem.action = @selector(performClose:);
}

- (void)hideMenuItem:(NSMenuItem*)menuItem {
  menuItem.hidden = YES;
  menuItem.tag = 0;
  menuItem.action = 0;
}

// Updates menu items in the File menu to match the main window.
- (void)updateMenuItemKeyEquivalents {
  // If the browser window has tabs, assign ⇧⌘W to "Close Window"
  // and ⌘W to "Close Tab", otherwise hide the "Close Tab" item and
  // assign ⌘W to "Close Window".
  //
  // One way to shuffle these shortcuts is to simply find the "Close Window"
  // and "Close Tab" menu items and change their key equivalents. For some
  // reason, the AppKit won't let us do that. For example, if the "Close Tab"
  // item has @"w" as its equivalent and we temporarily assign @"w" to
  // "Close Window", we can never set @"w" as the key equivalent for the
  // "Close Tab" item. It doesn't appear to be an issue with some other item
  // having that same equivalent, the AppKit just won't take it. We get around
  // this problem by leaving key equivalents alone and instead change the
  // titles and actions of the menu items that own those equivalents.
  NSMenuItem* cmdWMenuItem = [self cmdWMenuItem];
  NSMenuItem* shiftCmdWMenuItem = [self shiftCmdWMenuItem];

  // If we can't find a ⌘W or ⇧⌘W item with IDC_CLOSE_WINDOW or IDC_CLOSE_TAB
  // as the tag, assume the user has assigned a custom shortcut to one or both
  // of these items. If the user has assigned a custom shortcut, it doesn't
  // make sense to perform any shuffling.
  if (cmdWMenuItem == nil || shiftCmdWMenuItem == nil) {
    return;
  }

  if ([self windowHasBrowserTabs:[self windowForPerformClose]]) {
    // Close Window   ⇧⌘W
    // Close All     ⌥⇧⌘W (alternate, replaces "Close Window" when ⌥ pressed)
    // Close Tab       ⌘W
    [self configureMenuItemForCloseWindow:shiftCmdWMenuItem];
    [self configureMenuItemForCloseTab:cmdWMenuItem];
  } else {
    // (no ⇧⌘W menu item)
    // Close All     ⌥⇧⌘W (alternate, appears when ⌥ pressed)
    // Close Window    ⌘W
    //
    // Having "Close All" appear out of nowhere when the ⌥ key is pressed is
    // less than ideal, but "Close All" is a non-primary piece of UI and
    // non-tabbed windows are significantly less common, so this will do for
    // now. This may need to be revisited if it turns out to become an issue.
    [self hideMenuItem:shiftCmdWMenuItem];
    [self configureMenuItemForCloseWindow:cmdWMenuItem];
  }

  // Force no longer hidden items to appear, or newly hidden items to
  // disappear.
  [[self fileMenu] update];
}

// Create restorable state archives with secure encoding. See the article at
// https://sector7.computest.nl/post/2022-08-process-injection-breaking-all-macos-security-layers-with-a-single-vulnerability/
// for more details.
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication*)app {
  return YES;
}

- (BOOL)application:(NSApplication*)application
    willContinueUserActivityWithType:(NSString*)userActivityType {
  return [userActivityType isEqualToString:NSUserActivityTypeBrowsingWeb];
}

- (BOOL)application:(NSApplication*)application
    continueUserActivity:(NSUserActivity*)userActivity
      restorationHandler:
          (void (^)(NSArray<id<NSUserActivityRestoring>>*))restorationHandler
{
  if (![userActivity.activityType
          isEqualToString:NSUserActivityTypeBrowsingWeb]) {
    return NO;
  }

  NSURL* url = userActivity.webpageURL;
  if (!url)
    return NO;

  GURL gurl(base::SysNSStringToUTF8([url absoluteString]));
  StartupTabs tabs;
  tabs.emplace_back(gurl, /*is_untrusted_launch=*/true);

  [self openStartupTabsReplacingNTP:std::move(tabs)];
  return YES;
}

- (void)application:(NSApplication*)application
    didFailToContinueUserActivityWithType:(NSString*)userActivityType
                                    error:(NSError*)error {
}

#pragma mark - Handoff Manager

- (void)updateHandoffManagerWithURL:(const GURL&)handoffURL
                              title:(const std::u16string&)handoffTitle {
  [_handoffManager updateActiveURL:handoffURL];
  [_handoffManager updateActiveTitle:handoffTitle];
}

- (void)updateHandoffManager:(content::WebContents*)webContents {
  if (!_handoffManager)
    _handoffManager = [[HandoffManager alloc] init];

  if ([self isHandoffEligible:webContents]) {
    [self updateHandoffManagerWithURL:webContents->GetVisibleURL()
                                title:webContents->GetTitle()];
  } else {
    [self updateHandoffManagerWithURL:GURL() title:std::u16string()];
  }
}

- (BOOL)isHandoffEligible:(content::WebContents*)webContents {
  if (!webContents)
    return NO;

  Profile* profile =
      Profile::FromBrowserContext(webContents->GetBrowserContext());
  if (!profile)
    return NO;

  // Handoff is not allowed from an incognito profile. To err on the safe side,
  // also disallow Handoff from a guest profile.
  return profile->IsRegularProfile();
}

- (BOOL)isProfileReady {
  return !g_browser_process->browser_policy_connector()
              ->chrome_browser_cloud_management_controller()
              ->IsEnterpriseStartupDialogShowing();
}

#pragma mark - HandoffObserverDelegate

- (void)handoffContentsChanged:(content::WebContents*)webContents {
  [self updateHandoffManager:webContents];
}

#pragma mark - ASWebAuthenticationSessionWebBrowserSessionHandling

// Note that both of these WebAuthenticationSession calls come in on a random
// worker thread, so it's important to hop to the main thread.

- (void)beginHandlingWebAuthenticationSessionRequest:
    (ASWebAuthenticationSessionRequest*)request {
  dispatch_async(dispatch_get_main_queue(), ^{
    // Start tracking the pending request, so it's possible to cancel it before
    // the session actually starts.
    NSUUID* key = request.UUID;
    DCHECK(![GetPendingWebAuthRequests() objectForKey:key])
        << "Duplicate ASWebAuthenticationSessionRequest";
    [GetPendingWebAuthRequests() setObject:request forKey:key];

    app_controller_mac::RunInLastProfileSafely(
        base::BindOnce(&BeginHandlingWebAuthenticationSessionRequestWithProfile,
                       request),
        app_controller_mac::kShowProfilePickerOnFailure);
  });
}

- (void)cancelWebAuthenticationSessionRequest:
    (ASWebAuthenticationSessionRequest*)request {
  dispatch_async(dispatch_get_main_queue(), ^{
    if (NSUUID* key = request.UUID;
        [GetPendingWebAuthRequests() objectForKey:key]) {
      // If the web authentication request is still pending, remove it from the
      // list of pending requests and notify the OS that it was successfully
      // canceled.
      [GetPendingWebAuthRequests() removeObjectForKey:key];
      NSError* error = [NSError
          errorWithDomain:ASWebAuthenticationSessionErrorDomain
                     code:ASWebAuthenticationSessionErrorCodeCanceledLogin
                 userInfo:nil];
      [request cancelWithError:error];
    } else {
      // Otherwise, tell the web authentication request to cancel itself.
      AuthSessionRequest::CancelAuthSession(request);
    }
  });
}

- (ConfirmQuitPanelController*)confirmQuitPanelControllerForTesting {
  if (!_confirmQuitPanelController) {
    _confirmQuitPanelController = [[ConfirmQuitPanelController alloc] init];
  }
  return _confirmQuitPanelController;
}

- (void)setCmdWMenuItemForTesting:(NSMenuItem*)menuItem {
  _cmdWMenuItemForTesting = menuItem;
}

- (void)setShiftCmdWMenuItemForTesting:(NSMenuItem*)menuItem {
  _shiftCmdWMenuItemForTesting = menuItem;
}

- (void)setMainWindowForTesting:(NSWindow*)window {
  _mainWindowForTesting = window;
}

- (void)setLastProfileForTesting:(Profile*)profile {
  _lastProfile = profile;
  ProfileBrowserCollection* collection =
      ProfileBrowserCollection::GetForProfile(profile);
  if (!collection) {
    return;
  }
  _lastActiveBrowser = collection->GetLastActiveBrowser()->GetWeakPtr();
}

@end  // @implementation AppController

//---------------------------------------------------------------------------

namespace {

void UpdateProfileInUse(Profile* profile) {
  if (!profile) {
    return;
  }
  [AppController.sharedController setLastProfile:profile];
}

void OpenStartupTabsInBrowserWithProfile(const StartupTabs& tabs,
                                         Profile* profile) {
  if (!profile)
    return;  // No suitable profile to open the URLs, do nothing.
  // Prefer a regular (non-incognito) profile
  if (profile->IsIncognitoProfile()) {
    profile = ProfileManager::MaybeForceOffTheRecordMode(
        profile->GetOriginalProfile());
  }
  // Use FindTabbedBrowser() to ensure URLs open in a normal tabbed browser
  // window, not in PWA/app windows which cannot accept new tabs.
  BrowserWindowInterface* browser =
      ProfileBrowserCollection::GetForProfile(profile)->FindTabbedBrowser();
  int startupIndex = TabStripModel::kNoTab;
  content::WebContents* startupContent = nullptr;
  if (browser && browser->GetTabStripModel()->count() == 1) {
    // If there's only 1 tab and the tab is NTP, close this NTP tab and open all
    // startup urls in new tabs, because the omnibox will stay focused if we
    // load url in NTP tab.
    startupIndex = browser->GetTabStripModel()->active_index();
    startupContent = browser->GetTabStripModel()->GetActiveWebContents();
  } else if (!browser) {
    // if no browser window exists then create one with no tabs to be filled in.
    browser = CreateBrowserWindow(BrowserWindowCreateParams(profile, true));
    browser->GetWindow()->Show();
  }

  // Various methods to open URLs that we get in a native fashion. We use
  // StartupBrowserCreator here because on the other platforms, URLs to open
  // come through the ProcessSingleton, and it calls StartupBrowserCreator. It's
  // best to bottleneck the openings through that for uniform handling.
  base::CommandLine dummy(base::CommandLine::NO_PROGRAM);
  chrome::startup::IsFirstRun first_run =
      first_run::IsChromeFirstRun() ? chrome::startup::IsFirstRun::kYes
                                    : chrome::startup::IsFirstRun::kNo;
  StartupBrowserCreatorImpl launch(base::FilePath(), dummy, first_run);
  launch.OpenTabsInBrowser(browser->GetBrowserForMigrationOnly(),
                           chrome::startup::IsProcessStartup::kNo, tabs,
                           StartupBrowserCreatorImpl::TabOverWrite::kNo);

  // This NTP check should be replaced once https://crbug.com/41261582 is fixed.
  if (startupIndex != TabStripModel::kNoTab &&
      (startupContent->GetVisibleURL() == chrome::kChromeUINewTabURL ||
       startupContent->GetVisibleURL() == chrome::kChromeUINewTabPageURL)) {
    browser->GetTabStripModel()->CloseWebContentsAt(startupIndex,
                                                    TabCloseTypes::CLOSE_NONE);
  }
}

// Returns the profile to be used for new windows (or nullptr if it fails).
Profile* GetSafeProfile(Profile* loaded_profile) {
  return
      [AppController.sharedController safeProfileForNewWindows:loaded_profile];
}

// Called when the profile has been loaded for RunIn*ProfileSafely(). This
// profile may not be safe to use for new windows (due to policies).
void OnProfileLoaded(base::OnceCallback<void(Profile*)> callback,
                     app_controller_mac::ProfileLoadFailureBehavior on_failure,
                     Profile* loaded_profile) {
  Profile* safe_profile = GetSafeProfile(loaded_profile);
  if (!safe_profile) {
    switch (on_failure) {
      case app_controller_mac::kShowProfilePickerOnFailure:
        ProfilePicker::Show(ProfilePicker::Params::FromEntryPoint(
            ProfilePicker::EntryPoint::kUnableToCreateBrowser));
        break;

      case app_controller_mac::kIgnoreOnFailure:
        break;
    }
    std::move(callback).Run(nullptr);
    return;
  }

  // Shutdown may have started since this callback was scheduled.
  if (GetBrowserWindowCreationStatusForProfile(*safe_profile) !=
      Browser::CreationStatus::kOk) {
    std::move(callback).Run(nullptr);
    return;
  }

  std::move(callback).Run(safe_profile);
}

}  // namespace

namespace app_controller_mac {

bool IsOpeningNewWindow() {
  return g_is_opening_new_window;
}

void CreateGuestProfileIfNeeded() {
  g_browser_process->profile_manager()->CreateProfileAsync(
      ProfileManager::GetGuestProfilePath(),
      base::BindOnce(&UpdateProfileInUse));
}

void EnterpriseStartupDialogClosed() {
  CHECK(!g_browser_process->browser_policy_connector()
             ->chrome_browser_cloud_management_controller()
             ->IsEnterpriseStartupDialogShowing(),
        base::NotFatalUntil::M155);
  NSNotification* notify = [NSNotification
      notificationWithName:NSApplicationDidFinishLaunchingNotification
                    object:NSApp];
  [AppController.sharedController applicationDidFinishLaunching:notify];
}

void RunInLastProfileSafely(
    base::OnceCallback<void(Profile*)> callback,
    app_controller_mac::ProfileLoadFailureBehavior on_failure) {
  DCHECK(callback);
  if (Profile* profile = [AppController.sharedController lastProfileIfLoaded]) {
    OnProfileLoaded(std::move(callback), on_failure, profile);
    return;
  }

  g_browser_process->profile_manager()->CreateProfileAsync(
      GetStartupProfilePathMac(),
      base::BindOnce(&OnProfileLoaded, std::move(callback), on_failure));
}

void RunInProfileSafely(
    const base::FilePath& profile_dir,
    base::OnceCallback<void(Profile*)> callback,
    app_controller_mac::ProfileLoadFailureBehavior on_failure) {
  DCHECK(callback);
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  // `profile_manager` can be null in tests.
  if (!profile_manager) {
    OnProfileLoaded(std::move(callback), on_failure, nullptr);
    return;
  }
  if (Profile* profile = profile_manager->GetProfileByPath(profile_dir)) {
    OnProfileLoaded(std::move(callback), on_failure, profile);
    return;
  }
  g_browser_process->profile_manager()->LoadProfileByPath(
      profile_dir, /*incognito=*/false,
      base::BindOnce(&OnProfileLoaded, std::move(callback), on_failure));
}

void AllowApplicationToTerminate() {
  if (NSApp) {
    [AppController.sharedController allowApplicationToTerminate];
  } else {
    // Some test processes don't initialize NSApp, in which case accessing
    // sharedController would crash.
    CHECK_IS_TEST();
  }
}

// static
void TabRestorer::RestoreMostRecent(Profile* profile) {
  RestoreByID(profile, SessionID::InvalidValue());
}

// static
void TabRestorer::RestoreByID(Profile* profile, SessionID session_id) {
  DCHECK(profile);
  auto* service = TabRestoreServiceFactory::GetForProfile(profile);
  if (!service)
    return;
  if (service->IsLoaded()) {
    DoRestoreTab(profile, session_id);
  } else {
    // TabRestoreService isn't loaded. Tell it to load entries, and call
    // OpenWindowWithRestoredTabs() when it's done.
    std::ignore = new TabRestorer(profile, session_id);
    service->LoadTabsFromLastSession();
  }
}

// static
void TabRestorer::DoRestoreTab(Profile* profile, SessionID session_id) {
  DCHECK(profile);
  auto* service = TabRestoreServiceFactory::GetForProfile(profile);
  if (!service)
    return;
  BrowserWindowInterface* browser =
      ProfileBrowserCollection::GetForProfile(profile)->FindTabbedBrowser();
  sessions::LiveTabContext* context =
      browser ? browser->GetFeatures().live_tab_context() : nullptr;
  if (session_id.is_valid()) {
    service->RestoreEntryById(context, session_id,
                              WindowOpenDisposition::UNKNOWN);
  } else {
    service->RestoreMostRecentEntry(context);
  }
}

TabRestorer::TabRestorer(Profile* profile, SessionID session_id)
    : profile_(profile), session_id_(session_id) {
  auto* service = TabRestoreServiceFactory::GetForProfile(profile);
  DCHECK(service);
  observation_.Observe(service);
}

TabRestorer::~TabRestorer() = default;

void TabRestorer::TabRestoreServiceDestroyed(
    sessions::TabRestoreService* service) {
  delete this;
}

void TabRestorer::TabRestoreServiceLoaded(
    sessions::TabRestoreService* service) {
  observation_.Reset();
  DoRestoreTab(profile_, session_id_);
  delete this;
}

void ResetKeepAliveWhileHidden() {
  [AppController.sharedController resetKeepAliveWhileHidden];
}

}  // namespace app_controller_mac
