// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/app_shim/app_shim_controller.h"
#import <Cocoa/Cocoa.h>
#include <mach/message.h>
#include <utility>
#include "base/apple/bundle_locations.h"
#include "base/apple/foundation_util.h"
#include "base/apple/mach_logging.h"
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/hash/md5.h"
#include "base/mac/launch_application.h"
#include "base/mac/mac_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_param_associator.h"
#include "base/metrics/histogram_macros_local.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/synchronization/waitable_event.h"
#import "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/app_shim/app_shim_delegate.h"
#include "chrome/app_shim/app_shim_render_widget_host_view_mac_delegate.h"
#include "chrome/browser/ui/cocoa/browser_window_command_handler.h"
#include "chrome/browser/ui/cocoa/chrome_command_dispatcher_delegate.h"
#include "chrome/browser/ui/cocoa/main_menu_builder.h"
#import "chrome/browser/ui/cocoa/renderer_context_menu/chrome_swizzle_services_menu_updater.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/mac/app_mode_common.h"
#include "chrome/common/process_singleton_lock_posix.h"
#include "chrome/grit/generated_resources.h"
#import "chrome/services/mac_notifications/mac_notification_service_ns.h"
#import "chrome/services/mac_notifications/mac_notification_service_un.h"
#include "components/metrics/child_histogram_fetcher_impl.h"
#include "components/remote_cocoa/app_shim/application_bridge.h"
#include "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#include "components/remote_cocoa/common/application.mojom.h"
#include "components/variations/field_trial_config/field_trial_util.h"
#include "components/variations/variations_switches.h"
#include "content/public/browser/remote_cocoa.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/platform/named_platform_channel.h"
#include "mojo/public/cpp/platform/platform_channel.h"
#include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/screen.h"
#include "ui/gfx/image/image.h"
// The ProfileMenuTarget bridges between Objective C (as the target for the
// profile menu NSMenuItems) and C++ (the mojo methods called by
// AppShimController).
@interface ProfileMenuTarget : NSObject {
raw_ptr<AppShimController> _controller;
}
- (instancetype)initWithController:(AppShimController*)controller;
- (void)clearController;
@end
@implementation ProfileMenuTarget
- (instancetype)initWithController:(AppShimController*)controller {
if (self = [super init])
_controller = controller;
return self;
}
- (void)clearController {
_controller = nullptr;
}
- (void)profileMenuItemSelected:(id)sender {
if (_controller)
_controller->ProfileMenuItemSelected([sender tag]);
}
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
return YES;
}
@end
// The ApplicationDockMenuTarget bridges between Objective C (as the target for
// the profile menu NSMenuItems) and C++ (the mojo methods called by
// AppShimController).
@interface ApplicationDockMenuTarget : NSObject {
raw_ptr<AppShimController> _controller;
}
- (instancetype)initWithController:(AppShimController*)controller;
- (void)clearController;
@end
@implementation ApplicationDockMenuTarget
- (instancetype)initWithController:(AppShimController*)controller {
if (self = [super init])
_controller = controller;
return self;
}
- (void)clearController {
_controller = nullptr;
}
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
return YES;
}
- (void)commandFromDock:(id)sender {
if (_controller)
_controller->CommandFromDock([sender tag]);
}
@end
namespace {
// The maximum amount of time to wait for Chrome's AppShimListener to be
// ready.
constexpr base::TimeDelta kPollTimeoutSeconds = base::Seconds(60);
// The period in between attempts to check of Chrome's AppShimListener is
// ready.
constexpr base::TimeDelta kPollPeriodMsec = base::Milliseconds(100);
// Helper that keeps stops another sequence from executing any code while this
// object is alive. The constructor waits for the other thread to be blocked,
// while the destructor signals the other thread to continue running again.
class ScopedSynchronizeThreads {
public:
explicit ScopedSynchronizeThreads(
scoped_refptr<base::SequencedTaskRunner> thread_runner) {
// This event is signalled by a task posted to the other thread as soon as
// it starts executing, to signal that no more code is running on that
// thread. The main thread only proceeds after this event is signalled.
base::WaitableEvent thread_blocked;
// Heap allocate `operation_finished` and make sure it is destroyed on the
// thread that waits on that event. This ensures it isn't destroyed too
// early.
auto operation_finished = std::make_unique<base::WaitableEvent>();
operation_finished_ = operation_finished.get();
thread_runner->PostTask(
FROM_HERE,
base::BindOnce(
[](base::WaitableEvent* thread_blocked,
std::unique_ptr<base::WaitableEvent> operation_finished) {
thread_blocked->Signal();
operation_finished->Wait();
},
&thread_blocked, std::move(operation_finished)));
thread_blocked.Wait();
}
~ScopedSynchronizeThreads() { operation_finished_->Signal(); }
private:
// This event is signalled by the main thread to indicate that all the work is
// done, allowing the other thread to be unblocked again.
raw_ptr<base::WaitableEvent> operation_finished_;
};
} // namespace
AppShimController::Params::Params() = default;
AppShimController::Params::Params(const Params& other) = default;
AppShimController::Params::~Params() = default;
AppShimController::AppShimController(const Params& params)
: params_(params),
host_receiver_(host_.BindNewPipeAndPassReceiver()),
delegate_([[AppShimDelegate alloc] initWithController:this]),
profile_menu_target_([[ProfileMenuTarget alloc] initWithController:this]),
application_dock_menu_target_(
[[ApplicationDockMenuTarget alloc] initWithController:this]) {
screen_ = std::make_unique<display::ScopedNativeScreen>();
NSApp.delegate = delegate_;
[ChromeSwizzleServicesMenuUpdater install];
// Since this is early startup code, there is no guarantee that the state of
// the features being tested for here matches the state from the eventualy
// Chrome we connect to (although they will match the vast majority of the
// time). Creating the notification service when it ends up being not needed
// is harmless, and the only effect of not creating it early when we later
// need it is that we might miss some notification actions, which again is
// harmless.
if (base::FeatureList::IsEnabled(features::kAppShimNotificationAttribution) &&
WebAppIsAdHocSigned()) {
// `notification_service_` needs to be created early during start up to make
// sure it is able to install its delegate before the OS attempts to inform
// it of any notification actions that might have happened.
notification_service_ =
std::make_unique<mac_notifications::MacNotificationServiceUN>(
std::move(notification_action_handler_remote_),
base::BindRepeating(
&AppShimController::NotificationPermissionStatusChanged,
base::Unretained(this)),
UNUserNotificationCenter.currentNotificationCenter);
}
}
AppShimController::~AppShimController() {
// Un-set the delegate since NSApplication does not retain it.
NSApp.delegate = nil;
[profile_menu_target_ clearController];
[application_dock_menu_target_ clearController];
}
// static
void AppShimController::PreInitFeatureState(
const base::CommandLine& command_line) {
new base::FieldTrialList();
auto feature_list = std::make_unique<base::FeatureList>();
base::FeatureList::FailOnFeatureAccessWithoutFeatureList();
// App shims can generally be launched in one of two ways:
// - By chrome itself, in which case the full feature and field trial state
// is passed on the command line, and any state stored in user_data_dir is
// ignored. In this case we could avoid re-initializing feature and field
// trial state in FinalizeFeatureState entirely, but since this is the case
// with by far the most test coverage, doing so would make it much more
// likely that some change accidentally slips in that would breaks with the
// early access and reinitialization behavior.
// - By the OS or user directly. In which case a (possibly outdated) state is
// loaded from user_data_dir, but we do allow explicit feature and/or field
// trial overrides on the command line to help with manual
// testing/development.
//
// In both cases the state initialized here is only used during startup, and
// as soon as a mojo connection has been established with Chrome the final
// feature state is passed to FinalizeFeatureState below.
//
// Several integration tests launch app shims with somewhat of a mix of these
// two options. Where tests try to simulate an app shim being launched by the
// OS we still pass switches such as the kLaunchedByChromeProcessId (to ensure
// the app shim communicates with the correct test instance), but don't pass
// the full feature state on the command line, so having
// kLaunchedByChromeProcessId be present does not guarantee that feature state
// is passed on the command line as well.
// Add command line overrides. These will always be set if this app shim is
// launched by chrome, but for development/testing purposes can also be used
// to override state found in the user_data_dir file if the launch was not
// triggered by chrome. In either case, FinalizeFeatureState will reset all
// feature and field trial state to match the state of the running Chrome
// instance.
variations::VariationsCommandLine::GetForCommandLine(command_line)
.ApplyToFeatureAndFieldTrialList(feature_list.get());
// If the shim was launched by chrome, we're done. However if the shim was
// launched directly by the user/OS the command line parameters were merely
// optional overrides, so read state from the file in user_data_dir to get the
// correct feature and field trial state for features and field trials that
// have not already been explicitly overridden.
if (!command_line.HasSwitch(app_mode::kLaunchedByChromeProcessId)) {
auto file_state = variations::VariationsCommandLine::ReadFromFile(
base::PathService::CheckedGet(chrome::DIR_USER_DATA)
.Append(app_mode::kFeatureStateFileName));
if (file_state.has_value()) {
file_state->ApplyToFeatureAndFieldTrialList(feature_list.get());
}
}
// Until FinalizeFeatureState() is called, only features whose name is in the
// below list are allowed to be passed to base::FeatureList::IsEnabled().
// Attempts to check the state of any other feature will behave as if no
// FeatureList was set yet at all (i.e. check-fail).
base::FeatureList::SetEarlyAccessInstance(
std::move(feature_list),
{"AppShimLaunchChromeSilently", "AppShimNotificationAttribution",
"DcheckIsFatal", "MojoBindingsInlineSLS", "MojoInlineMessagePayloads",
"MojoIpcz", "MojoIpczMemV2", "MojoTaskPerMessage",
"StandardCompliantHostCharacters",
"StandardCompliantNonSpecialSchemeURLParsing",
"UseAdHocSigningForWebAppShims", "UseIDNA2008NonTransitional",
"SonomaAccessibilityActivationRefinements"});
}
// static
void AppShimController::FinalizeFeatureState(
const variations::VariationsCommandLine& feature_state,
const scoped_refptr<base::SequencedTaskRunner>& io_thread_runner) {
// This code assumes no other threads are running. So make sure there is no
// started ThreadPoolInstance, and block the IO thread for the duration of
// this method.
CHECK(!base::ThreadPoolInstance::Get() ||
!base::ThreadPoolInstance::Get()->WasStarted());
ScopedSynchronizeThreads block_io_thread(io_thread_runner);
// Recreate FieldTrialList.
std::unique_ptr<base::FieldTrialList> old_field_trial_list(
base::FieldTrialList::ResetInstance());
CHECK(old_field_trial_list);
// This is intentionally leaked since it needs to live for the duration of
// the app shim process and there's no benefit in cleaning it up at exit.
auto* field_trial_list = new base::FieldTrialList();
ANNOTATE_LEAKING_OBJECT_PTR(field_trial_list);
std::ignore = field_trial_list;
// Reset FieldTrial parameter cache.
base::FieldTrialParamAssociator::GetInstance()->ClearAllCachedParams({});
// Create a new FeatureList and field trial state using what was passed by the
// browser process.
auto feature_list = std::make_unique<base::FeatureList>();
feature_state.ApplyToFeatureAndFieldTrialList(feature_list.get());
base::FeatureList::SetInstance(std::move(feature_list));
}
void AppShimController::OnAppFinishedLaunching(
bool launched_by_notification_action) {
DCHECK_EQ(init_state_, InitState::kWaitingForAppToFinishLaunch);
init_state_ = InitState::kWaitingForChromeReady;
launched_by_notification_action_ = launched_by_notification_action;
if (FindOrLaunchChrome()) {
// Start polling to see if Chrome is ready to connect.
PollForChromeReady(kPollTimeoutSeconds);
}
// Otherwise, Chrome is in the process of launching and `PollForChromeReady`
// will be called when launching is complete.
}
bool AppShimController::FindOrLaunchChrome() {
DCHECK(!chrome_to_connect_to_);
DCHECK(!chrome_launched_by_app_);
const base::CommandLine* app_command_line =
base::CommandLine::ForCurrentProcess();
// If this shim was launched by Chrome, only connect to that that specific
// process.
if (app_command_line->HasSwitch(app_mode::kLaunchedByChromeProcessId)) {
std::string chrome_pid_string = app_command_line->GetSwitchValueASCII(
app_mode::kLaunchedByChromeProcessId);
int chrome_pid;
if (!base::StringToInt(chrome_pid_string, &chrome_pid)) {
LOG(FATAL) << "Invalid PID: " << chrome_pid_string;
}
chrome_to_connect_to_ = [NSRunningApplication
runningApplicationWithProcessIdentifier:chrome_pid];
if (!chrome_to_connect_to_) {
// Sometimes runningApplicationWithProcessIdentifier fails to return the
// application, even though it exists. If that happens, try to find the
// running application in the full list of running applications manually.
// See https://crbug.com/1426897.
NSArray<NSRunningApplication*>* apps =
NSWorkspace.sharedWorkspace.runningApplications;
for (unsigned i = 0; i < apps.count; ++i) {
if (apps[i].processIdentifier == chrome_pid) {
chrome_to_connect_to_ = apps[i];
}
}
if (!chrome_to_connect_to_) {
LOG(FATAL) << "Failed to open process with PID: " << chrome_pid;
}
}
return true;
}
// Query the singleton lock. If the lock exists and specifies a running
// Chrome, then connect to that process. Otherwise, launch a new Chrome
// process.
chrome_to_connect_to_ = FindChromeFromSingletonLock(params_.user_data_dir);
if (chrome_to_connect_to_) {
return true;
}
// In tests, launching Chrome does nothing.
if (app_command_line->HasSwitch(app_mode::kLaunchedForTest)) {
return true;
}
// Otherwise, launch Chrome.
base::FilePath chrome_bundle_path = base::apple::OuterBundlePath();
LOG(INFO) << "Launching " << chrome_bundle_path.value();
base::CommandLine browser_command_line(base::CommandLine::NO_PROGRAM);
browser_command_line.AppendSwitchPath(switches::kUserDataDir,
params_.user_data_dir);
// Forward feature and field trial related switches to Chrome to aid in
// testing and development with custom feature or field trial configurations.
static constexpr const char* switches_to_forward[] = {
switches::kEnableFeatures, switches::kDisableFeatures,
switches::kForceFieldTrials,
variations::switches::kForceFieldTrialParams};
for (const char* switch_name : switches_to_forward) {
if (app_command_line->HasSwitch(switch_name)) {
browser_command_line.AppendSwitchASCII(
switch_name, app_command_line->GetSwitchValueASCII(switch_name));
}
}
const bool silent_chrome_launch =
base::FeatureList::IsEnabled(features::kAppShimLaunchChromeSilently);
if (silent_chrome_launch) {
browser_command_line.AppendSwitch(switches::kNoStartupWindow);
}
base::mac::LaunchApplication(
chrome_bundle_path, browser_command_line, /*url_specs=*/{},
{.create_new_instance = true,
.hidden_in_background = silent_chrome_launch},
base::BindOnce(
[](AppShimController* shim_controller, NSRunningApplication* app,
NSError* error) {
if (error) {
LOG(FATAL) << "Failed to launch Chrome.";
}
shim_controller->chrome_launched_by_app_ = app;
// Start polling to see if Chrome is ready to connect.
shim_controller->PollForChromeReady(kPollTimeoutSeconds);
},
// base::Unretained is safe because this is a singleton.
base::Unretained(this)));
return false;
}
// static
NSRunningApplication* AppShimController::FindChromeFromSingletonLock(
const base::FilePath& user_data_dir) {
base::FilePath lock_symlink_path =
user_data_dir.Append(chrome::kSingletonLockFilename);
std::string hostname;
int pid = -1;
if (!ParseProcessSingletonLock(lock_symlink_path, &hostname, &pid)) {
// This indicates that there is no Chrome process running (or that has been
// running long enough to get the lock).
LOG(INFO) << "Singleton lock not found at " << lock_symlink_path.value();
return nil;
}
// Open the associated pid. This could be invalid if Chrome terminated
// abnormally and didn't clean up.
NSRunningApplication* process_from_lock =
[NSRunningApplication runningApplicationWithProcessIdentifier:pid];
if (!process_from_lock) {
LOG(WARNING) << "Singleton lock pid " << pid << " invalid.";
return nil;
}
// Check the process' bundle id. As above, the specified pid could have been
// reused by some other process.
NSString* expected_bundle_id = base::apple::OuterBundle().bundleIdentifier;
NSString* lock_bundle_id = process_from_lock.bundleIdentifier;
if (![expected_bundle_id isEqualToString:lock_bundle_id]) {
LOG(WARNING) << "Singleton lock pid " << pid
<< " has unexpected bundle id.";
return nil;
}
return process_from_lock;
}
void AppShimController::PollForChromeReady(
const base::TimeDelta& time_until_timeout) {
// If the Chrome process we planned to connect to is not running anymore,
// quit.
if (chrome_to_connect_to_ && chrome_to_connect_to_.terminated) {
LOG(FATAL) << "Running chrome instance terminated before connecting.";
}
// If we launched a Chrome process and it has terminated, then that most
// likely means that it did not get the singleton lock (which means that we
// should find the processes that did below).
bool launched_chrome_is_terminated =
chrome_launched_by_app_ && chrome_launched_by_app_.terminated;
// If we haven't found the Chrome process that got the singleton lock, check
// now.
if (!chrome_to_connect_to_)
chrome_to_connect_to_ = FindChromeFromSingletonLock(params_.user_data_dir);
// If our launched Chrome has terminated, then there should have existed a
// process holding the singleton lock.
if (launched_chrome_is_terminated && !chrome_to_connect_to_)
LOG(FATAL) << "Launched Chrome has exited and singleton lock not taken.";
// Poll to see if the mojo channel is ready. Of note is that we don't actually
// verify that |endpoint| is connected to |chrome_to_connect_to_|.
{
mojo::PlatformChannelEndpoint endpoint;
NSString* browser_bundle_id =
base::apple::ObjCCast<NSString>([NSBundle.mainBundle
objectForInfoDictionaryKey:app_mode::kBrowserBundleIDKey]);
CHECK(browser_bundle_id);
const std::string server_name = base::StringPrintf(
"%s.%s.%s", base::SysNSStringToUTF8(browser_bundle_id).c_str(),
app_mode::kAppShimBootstrapNameFragment,
base::MD5String(params_.user_data_dir.value()).c_str());
endpoint = ConnectToBrowser(server_name);
if (endpoint.is_valid()) {
LOG(INFO) << "Connected to " << server_name;
SendBootstrapOnShimConnected(std::move(endpoint));
return;
}
}
// Otherwise, try again after a brief delay.
if (time_until_timeout < kPollPeriodMsec)
LOG(FATAL) << "Timed out waiting for running chrome instance to be ready.";
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AppShimController::PollForChromeReady,
base::Unretained(this),
time_until_timeout - kPollPeriodMsec),
kPollPeriodMsec);
}
// static
mojo::PlatformChannelEndpoint AppShimController::ConnectToBrowser(
const mojo::NamedPlatformChannel::ServerName& server_name) {
// Normally NamedPlatformChannel is used for point-to-point peer
// communication. For apps shims, the same server is used to establish
// connections between multiple shim clients and the server. To do this,
// the shim creates a local PlatformChannel and sends the local (send)
// endpoint to the server in a raw Mach message. The server uses that to
// establish an IsolatedConnection, which the client does as well with the
// remote (receive) end.
mojo::PlatformChannelEndpoint server_endpoint =
mojo::NamedPlatformChannel::ConnectToServer(server_name);
// The browser may still be in the process of launching, so the endpoint
// may not yet be available.
if (!server_endpoint.is_valid())
return mojo::PlatformChannelEndpoint();
mojo::PlatformChannel channel;
mach_msg_base_t message{};
message.header.msgh_id = app_mode::kBootstrapMsgId;
message.header.msgh_bits =
MACH_MSGH_BITS(MACH_MSG_TYPE_MOVE_SEND, MACH_MSG_TYPE_MOVE_SEND);
message.header.msgh_size = sizeof(message);
message.header.msgh_local_port =
channel.TakeLocalEndpoint().TakePlatformHandle().ReleaseMachSendRight();
message.header.msgh_remote_port =
server_endpoint.TakePlatformHandle().ReleaseMachSendRight();
kern_return_t kr = mach_msg_send(&message.header);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_msg_send";
return mojo::PlatformChannelEndpoint();
}
return channel.TakeRemoteEndpoint();
}
void AppShimController::SendBootstrapOnShimConnected(
mojo::PlatformChannelEndpoint endpoint) {
DCHECK_EQ(init_state_, InitState::kWaitingForChromeReady);
init_state_ = InitState::kHasSentOnShimConnected;
// Chrome will relaunch shims when relaunching apps.
[NSApp disableRelaunchOnLogin];
CHECK(!params_.user_data_dir.empty());
mojo::ScopedMessagePipeHandle message_pipe =
bootstrap_mojo_connection_.Connect(std::move(endpoint));
CHECK(message_pipe.is_valid());
host_bootstrap_.Bind(mojo::PendingRemote<chrome::mojom::AppShimHostBootstrap>(
std::move(message_pipe), 0));
host_bootstrap_.set_disconnect_with_reason_handler(base::BindOnce(
&AppShimController::BootstrapChannelError, base::Unretained(this)));
auto app_shim_info = chrome::mojom::AppShimInfo::New();
app_shim_info->profile_path = params_.profile_dir;
app_shim_info->app_id = params_.app_id;
app_shim_info->app_url = params_.app_url;
// If the app shim was launched for a notification action, we don't want to
// automatically launch the app as well. So do a kRegisterOnly launch
// instead.
app_shim_info->launch_type =
launched_by_notification_action_
? chrome::mojom::AppShimLaunchType::kNotificationAction
: (base::CommandLine::ForCurrentProcess()->HasSwitch(
app_mode::kLaunchedByChromeProcessId) &&
!base::CommandLine::ForCurrentProcess()->HasSwitch(
app_mode::kIsNormalLaunch))
? chrome::mojom::AppShimLaunchType::kRegisterOnly
: chrome::mojom::AppShimLaunchType::kNormal;
app_shim_info->files = launch_files_;
app_shim_info->urls = launch_urls_;
if (base::mac::WasLaunchedAsHiddenLoginItem()) {
app_shim_info->login_item_restore_state =
chrome::mojom::AppShimLoginItemRestoreState::kHidden;
} else if (base::mac::WasLaunchedAsLoginOrResumeItem()) {
app_shim_info->login_item_restore_state =
chrome::mojom::AppShimLoginItemRestoreState::kWindowed;
} else {
app_shim_info->login_item_restore_state =
chrome::mojom::AppShimLoginItemRestoreState::kNone;
}
app_shim_info->notification_action_handler =
std::move(notification_action_handler_receiver_);
host_bootstrap_->OnShimConnected(
std::move(host_receiver_), std::move(app_shim_info),
base::BindOnce(&AppShimController::OnShimConnectedResponse,
base::Unretained(this)));
LOG(INFO) << "Sent OnShimConnected";
}
void AppShimController::SetUpMenu() {
chrome::BuildMainMenu(NSApp, delegate_, params_.app_name, true);
UpdateProfileMenu(std::vector<chrome::mojom::ProfileMenuItemPtr>());
}
void AppShimController::BootstrapChannelError(uint32_t custom_reason,
const std::string& description) {
// The bootstrap channel is expected to close after the response to
// OnShimConnected is received.
if (init_state_ == InitState::kHasReceivedOnShimConnectedResponse)
return;
LOG(ERROR) << "Bootstrap Channel error custom_reason:" << custom_reason
<< " description: " << description;
[NSApp terminate:nil];
}
void AppShimController::ChannelError(uint32_t custom_reason,
const std::string& description) {
LOG(ERROR) << "Channel error custom_reason:" << custom_reason
<< " description: " << description;
[NSApp terminate:nil];
}
void AppShimController::OnShimConnectedResponse(
chrome::mojom::AppShimLaunchResult result,
variations::VariationsCommandLine feature_state,
mojo::PendingReceiver<chrome::mojom::AppShim> app_shim_receiver) {
LOG(INFO) << "Received OnShimConnected.";
DCHECK_EQ(init_state_, InitState::kHasSentOnShimConnected);
init_state_ = InitState::kHasReceivedOnShimConnectedResponse;
// Finalize feature state and finish up initialization that was deferred for
// feature state to be fully setup.
FinalizeFeatureState(feature_state, params_.io_thread_runner);
base::ThreadPoolInstance::Get()->StartWithDefaultParams();
SetUpMenu();
if (result != chrome::mojom::AppShimLaunchResult::kSuccess) {
switch (result) {
case chrome::mojom::AppShimLaunchResult::kSuccess:
break;
case chrome::mojom::AppShimLaunchResult::kSuccessAndDisconnect:
LOG(ERROR) << "Launched successfully, but do not maintain connection.";
break;
case chrome::mojom::AppShimLaunchResult::kDuplicateHost:
LOG(ERROR) << "An AppShimHostBootstrap already exists for this app.";
break;
case chrome::mojom::AppShimLaunchResult::kProfileNotFound:
LOG(ERROR) << "No suitable profile found.";
break;
case chrome::mojom::AppShimLaunchResult::kAppNotFound:
LOG(ERROR) << "App not installed for specified profile.";
break;
case chrome::mojom::AppShimLaunchResult::kProfileLocked:
LOG(ERROR) << "Profile locked.";
break;
case chrome::mojom::AppShimLaunchResult::kFailedValidation:
LOG(ERROR) << "Validation failed.";
break;
};
[NSApp terminate:nil];
return;
}
shim_receiver_.Bind(std::move(app_shim_receiver),
ui::WindowResizeHelperMac::Get()->task_runner());
shim_receiver_.set_disconnect_with_reason_handler(
base::BindOnce(&AppShimController::ChannelError, base::Unretained(this)));
host_bootstrap_.reset();
}
void AppShimController::CreateRemoteCocoaApplication(
mojo::PendingAssociatedReceiver<remote_cocoa::mojom::Application>
receiver) {
remote_cocoa::ApplicationBridge::Get()->BindReceiver(std::move(receiver));
remote_cocoa::ApplicationBridge::Get()->SetContentNSViewCreateCallbacks(
base::BindRepeating(&AppShimController::CreateRenderWidgetHostNSView),
base::BindRepeating(remote_cocoa::CreateWebContentsNSView));
}
void AppShimController::CreateRenderWidgetHostNSView(
uint64_t view_id,
mojo::ScopedInterfaceEndpointHandle host_handle,
mojo::ScopedInterfaceEndpointHandle view_request_handle) {
remote_cocoa::RenderWidgetHostViewMacDelegateCallback
responder_delegate_creation_callback =
base::BindOnce(&AppShimController::GetDelegateForHost, view_id);
remote_cocoa::CreateRenderWidgetHostNSView(
view_id, std::move(host_handle), std::move(view_request_handle),
std::move(responder_delegate_creation_callback));
}
NSObject<RenderWidgetHostViewMacDelegate>*
AppShimController::GetDelegateForHost(uint64_t view_id) {
return [[AppShimRenderWidgetHostViewMacDelegate alloc]
initWithRenderWidgetHostNSViewID:view_id];
}
void AppShimController::CreateCommandDispatcherForWidget(uint64_t widget_id) {
if (auto* bridge =
remote_cocoa::NativeWidgetNSWindowBridge::GetFromId(widget_id)) {
bridge->SetCommandDispatcher([[ChromeCommandDispatcherDelegate alloc] init],
[[BrowserWindowCommandHandler alloc] init]);
} else {
LOG(ERROR) << "Failed to find host for command dispatcher.";
}
}
void AppShimController::SetBadgeLabel(const std::string& badge_label) {
NSApp.dockTile.badgeLabel = base::SysUTF8ToNSString(badge_label);
}
void AppShimController::UpdateProfileMenu(
std::vector<chrome::mojom::ProfileMenuItemPtr> profile_menu_items) {
profile_menu_items_ = std::move(profile_menu_items);
NSMenuItem* cocoa_profile_menu =
[NSApp.mainMenu itemWithTag:IDC_PROFILE_MAIN_MENU];
if (profile_menu_items_.empty()) {
cocoa_profile_menu.submenu = nil;
cocoa_profile_menu.hidden = YES;
return;
}
cocoa_profile_menu.hidden = NO;
NSMenu* menu = [[NSMenu alloc]
initWithTitle:l10n_util::GetNSStringWithFixup(IDS_PROFILES_MENU_NAME)];
[cocoa_profile_menu setSubmenu:menu];
// Note that this code to create menu items is nearly identical to the code
// in ProfileMenuController in the browser process.
for (size_t i = 0; i < profile_menu_items_.size(); ++i) {
const auto& mojo_item = profile_menu_items_[i];
NSString* name = base::SysUTF16ToNSString(mojo_item->name);
NSMenuItem* item =
[[NSMenuItem alloc] initWithTitle:name
action:@selector(profileMenuItemSelected:)
keyEquivalent:@""];
item.tag = mojo_item->menu_index;
item.state =
mojo_item->active ? NSControlStateValueOn : NSControlStateValueOff;
item.target = profile_menu_target_;
gfx::Image icon(mojo_item->icon);
item.image = icon.AsNSImage();
[menu insertItem:item atIndex:i];
}
}
void AppShimController::UpdateApplicationDockMenu(
std::vector<chrome::mojom::ApplicationDockMenuItemPtr> dock_menu_items) {
dock_menu_items_ = std::move(dock_menu_items);
}
void AppShimController::BindNotificationProvider(
mojo::PendingReceiver<mac_notifications::mojom::MacNotificationProvider>
provider) {
notifications_receiver_.reset();
notifications_receiver_.Bind(std::move(provider));
}
void AppShimController::RequestNotificationPermission(
RequestNotificationPermissionCallback callback) {
if (!notification_service_un()) {
std::move(callback).Run(
mac_notifications::mojom::RequestPermissionResult::kRequestFailed);
return;
}
notification_service_un()->RequestPermission(std::move(callback));
}
void AppShimController::BindNotificationService(
mojo::PendingReceiver<mac_notifications::mojom::MacNotificationService>
service,
mojo::PendingRemote<mac_notifications::mojom::MacNotificationActionHandler>
handler) {
CHECK(
base::FeatureList::IsEnabled(features::kAppShimNotificationAttribution));
// TODO(crbug.com/40616749): Once ad-hoc signed app shims become the
// default on supported platforms, change this to always use the
// UNUserNotification API (and not support notification attribution on other
// platforms at all).
if (WebAppIsAdHocSigned()) {
// While the constructor should have created the `notification_service_`
// instance already, it is possible that the base::FeatureList state at the
// time did not match the current Chrome state, so make sure to create the
// service now if it wasn't created already.
if (!notification_service_) {
CHECK(notification_action_handler_remote_);
notification_service_ =
std::make_unique<mac_notifications::MacNotificationServiceUN>(
std::move(notification_action_handler_remote_),
base::BindRepeating(
&AppShimController::NotificationPermissionStatusChanged,
base::Unretained(this)),
UNUserNotificationCenter.currentNotificationCenter);
}
// Note that `handler` as passed in to this method is ignored. Notification
// actions instead will be dispatched to the app-shim scoped mojo pipe that
// was established earlier during startup, to allow notification actions to
// be triggered before the browser process tries to connect to the
// notification service.
notification_service_un()->Bind(std::move(service));
// TODO(crbug.com/40616749): Determine when to ask for permissions.
notification_service_un()->RequestPermission(base::DoNothing());
} else {
// NSUserNotificationCenter is in the process of being replaced, and
// warnings about its deprecation are not helpful. https://crbug.com/1127306
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
notification_service_ =
std::make_unique<mac_notifications::MacNotificationServiceNS>(
std::move(service), std::move(handler),
[NSUserNotificationCenter defaultUserNotificationCenter]);
#pragma clang diagnostic pop
}
}
mac_notifications::MacNotificationServiceUN*
AppShimController::notification_service_un() {
if (!WebAppIsAdHocSigned()) {
return nullptr;
}
return static_cast<mac_notifications::MacNotificationServiceUN*>(
notification_service_.get());
}
void AppShimController::NotificationPermissionStatusChanged(
mac_notifications::mojom::PermissionStatus status) {
host_->NotificationPermissionStatusChanged(status);
}
void AppShimController::SetUserAttention(
chrome::mojom::AppShimAttentionType attention_type) {
switch (attention_type) {
case chrome::mojom::AppShimAttentionType::kCancel:
[NSApp cancelUserAttentionRequest:attention_request_id_];
attention_request_id_ = 0;
break;
case chrome::mojom::AppShimAttentionType::kCritical:
attention_request_id_ = [NSApp requestUserAttention:NSCriticalRequest];
break;
}
}
void AppShimController::OpenFiles(const std::vector<base::FilePath>& files) {
if (init_state_ == InitState::kWaitingForAppToFinishLaunch) {
launch_files_ = files;
} else {
host_->FilesOpened(files);
}
}
void AppShimController::ProfileMenuItemSelected(uint32_t index) {
for (const auto& mojo_item : profile_menu_items_) {
if (mojo_item->menu_index == index) {
host_->ProfileSelectedFromMenu(mojo_item->profile_path);
return;
}
}
}
void AppShimController::OpenUrls(const std::vector<GURL>& urls) {
if (init_state_ == InitState::kWaitingForAppToFinishLaunch) {
launch_urls_ = urls;
} else {
host_->UrlsOpened(urls);
}
}
void AppShimController::CommandFromDock(uint32_t index) {
DCHECK(0 <= index && index < dock_menu_items_.size());
DCHECK(init_state_ != InitState::kWaitingForAppToFinishLaunch);
[NSApp activateIgnoringOtherApps:YES];
host_->OpenAppWithOverrideUrl(dock_menu_items_[index]->url);
}
void AppShimController::CommandDispatch(int command_id) {
switch (command_id) {
case IDC_WEB_APP_SETTINGS:
host_->OpenAppSettings();
break;
case IDC_NEW_WINDOW:
host_->ReopenApp();
break;
}
}
NSMenu* AppShimController::GetApplicationDockMenu() {
if (init_state_ == InitState::kWaitingForAppToFinishLaunch ||
dock_menu_items_.size() == 0)
return nullptr;
NSMenu* dockMenu = [[NSMenu alloc] initWithTitle:@""];
for (size_t i = 0; i < dock_menu_items_.size(); ++i) {
const auto& mojo_item = dock_menu_items_[i];
NSString* name = base::SysUTF16ToNSString(mojo_item->name);
NSMenuItem* item =
[[NSMenuItem alloc] initWithTitle:name
action:@selector(commandFromDock:)
keyEquivalent:@""];
item.tag = i;
item.target = application_dock_menu_target_;
item.enabled =
[application_dock_menu_target_ validateUserInterfaceItem:item];
[dockMenu addItem:item];
}
return dockMenu;
}
void AppShimController::ApplicationWillTerminate() {
// Local histogram to let tests verify that histograms are emitted properly.
LOCAL_HISTOGRAM_BOOLEAN("AppShim.WillTerminate", true);
host_->ApplicationWillTerminate();
}
void AppShimController::BindChildHistogramFetcherFactory(
mojo::PendingReceiver<metrics::mojom::ChildHistogramFetcherFactory>
receiver) {
metrics::ChildHistogramFetcherFactoryImpl::Create(std::move(receiver));
}
bool AppShimController::WebAppIsAdHocSigned() const {
NSNumber* isAdHocSigned =
NSBundle.mainBundle.infoDictionary[app_mode::kCrAppModeIsAdHocSignedKey];
return isAdHocSigned.boolValue;
}