Windows should animate when they are about to get docked at screen edges.
[chromium-blink-merge.git] / apps / app_shim / chrome_main_app_mode_mac.mm
blobddad52a14822ac15e92c6c9d9c53d1c9b0d91bc0
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 // On Mac, one can't make shortcuts with command-line arguments. Instead, we
6 // produce small app bundles which locate the Chromium framework and load it,
7 // passing the appropriate data. This is the entry point into the framework for
8 // those app bundles.
10 #import <Cocoa/Cocoa.h>
12 #include "apps/app_shim/app_shim_messages.h"
13 #include "base/at_exit.h"
14 #include "base/command_line.h"
15 #include "base/logging.h"
16 #include "base/mac/bundle_locations.h"
17 #include "base/mac/launch_services_util.h"
18 #include "base/mac/mac_logging.h"
19 #include "base/mac/mac_util.h"
20 #include "base/mac/scoped_nsautorelease_pool.h"
21 #include "base/mac/scoped_nsobject.h"
22 #include "base/message_loop/message_loop.h"
23 #include "base/path_service.h"
24 #include "base/strings/sys_string_conversions.h"
25 #include "base/threading/thread.h"
26 #include "chrome/common/chrome_constants.h"
27 #include "chrome/common/chrome_paths.h"
28 #include "chrome/common/chrome_switches.h"
29 #include "chrome/common/mac/app_mode_common.h"
30 #include "grit/generated_resources.h"
31 #include "ipc/ipc_channel_proxy.h"
32 #include "ipc/ipc_listener.h"
33 #include "ipc/ipc_message.h"
34 #include "ui/base/resource/resource_bundle.h"
35 #include "ui/base/l10n/l10n_util.h"
37 namespace {
39 const app_mode::ChromeAppModeInfo* g_info;
40 base::Thread* g_io_thread = NULL;
42 }  // namespace
44 class AppShimController;
46 @interface AppShimDelegate : NSObject<NSApplicationDelegate> {
47  @private
48   AppShimController* appShimController_;  // Weak. Owns us.
49   BOOL terminateNow_;
50   BOOL terminateRequested_;
53 - (id)initWithController:(AppShimController*)controller;
54 - (BOOL)applicationOpenUntitledFile:(NSApplication *)app;
55 - (void)applicationWillBecomeActive:(NSNotification*)notification;
56 - (void)applicationWillHide:(NSNotification*)notification;
57 - (void)applicationWillUnhide:(NSNotification*)notification;
58 - (void)terminateNow;
60 @end
62 // The AppShimController is responsible for communication with the main Chrome
63 // process, and generally controls the lifetime of the app shim process.
64 class AppShimController : public IPC::Listener {
65  public:
66   AppShimController();
68   // Connects to Chrome and sends a LaunchApp message.
69   void Init();
71   // Builds main menu bar items.
72   void SetUpMenu();
74   void SendSetAppHidden(bool hidden);
76   void SendQuitApp();
78   // Called when the app is activated, either by the user clicking on it in the
79   // dock or by Cmd+Tabbing to it.
80   void ActivateApp(bool is_reopen);
82  private:
83   // IPC::Listener implemetation.
84   virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE;
85   virtual void OnChannelError() OVERRIDE;
87   // If Chrome failed to launch the app, |success| will be false and the app
88   // shim process should die.
89   void OnLaunchAppDone(apps::AppShimLaunchResult result);
91   // Terminates the app shim process.
92   void Close();
94   IPC::ChannelProxy* channel_;
95   base::scoped_nsobject<AppShimDelegate> nsapp_delegate_;
96   bool launch_app_done_;
98   DISALLOW_COPY_AND_ASSIGN(AppShimController);
101 AppShimController::AppShimController() : channel_(NULL),
102                                          launch_app_done_(false) {}
104 void AppShimController::Init() {
105   DCHECK(g_io_thread);
107   SetUpMenu();
109   // The user_data_dir for shims actually contains the app_data_path.
110   // I.e. <user_data_dir>/<profile_dir>/Web Applications/_crx_extensionid/
111   base::FilePath user_data_dir =
112       g_info->user_data_dir.DirName().DirName().DirName();
113   CHECK(!user_data_dir.empty());
115   base::FilePath socket_path =
116       user_data_dir.Append(app_mode::kAppShimSocketName);
117   IPC::ChannelHandle handle(socket_path.value());
118   channel_ = new IPC::ChannelProxy(handle, IPC::Channel::MODE_NAMED_CLIENT,
119       this, g_io_thread->message_loop_proxy().get());
121   channel_->Send(new AppShimHostMsg_LaunchApp(
122       g_info->profile_dir, g_info->app_mode_id,
123       CommandLine::ForCurrentProcess()->HasSwitch(app_mode::kNoLaunchApp) ?
124           apps::APP_SHIM_LAUNCH_REGISTER_ONLY : apps::APP_SHIM_LAUNCH_NORMAL));
126   nsapp_delegate_.reset([[AppShimDelegate alloc] initWithController:this]);
127   DCHECK(![NSApp delegate]);
128   [NSApp setDelegate:nsapp_delegate_];
131 void AppShimController::SetUpMenu() {
132   NSString* title = base::SysUTF16ToNSString(g_info->app_mode_name);
134   // Create a main menu since [NSApp mainMenu] is nil.
135   base::scoped_nsobject<NSMenu> main_menu([[NSMenu alloc] initWithTitle:title]);
137   // The title of the first item is replaced by OSX with the name of the app and
138   // bold styling. Create a dummy item for this and make it hidden.
139   NSMenuItem* dummy_item = [main_menu addItemWithTitle:title
140                                                 action:nil
141                                          keyEquivalent:@""];
142   base::scoped_nsobject<NSMenu> dummy_submenu(
143       [[NSMenu alloc] initWithTitle:title]);
144   [dummy_item setSubmenu:dummy_submenu];
145   [dummy_item setHidden:YES];
147   // Construct an unbolded app menu, to match how it appears in the Chrome menu
148   // bar when the app is focused.
149   NSMenuItem* item = [main_menu addItemWithTitle:title
150                                           action:nil
151                                    keyEquivalent:@""];
152   base::scoped_nsobject<NSMenu> submenu([[NSMenu alloc] initWithTitle:title]);
153   [item setSubmenu:submenu];
155   // Add a quit entry.
156   NSString* quit_localized_string =
157       l10n_util::GetNSStringF(IDS_EXIT_MAC, g_info->app_mode_name);
158   [submenu addItemWithTitle:quit_localized_string
159                      action:@selector(terminate:)
160               keyEquivalent:@"q"];
162   [NSApp setMainMenu:main_menu];
165 void AppShimController::SendQuitApp() {
166   channel_->Send(new AppShimHostMsg_QuitApp);
169 bool AppShimController::OnMessageReceived(const IPC::Message& message) {
170   bool handled = true;
171   IPC_BEGIN_MESSAGE_MAP(AppShimController, message)
172     IPC_MESSAGE_HANDLER(AppShimMsg_LaunchApp_Done, OnLaunchAppDone)
173     IPC_MESSAGE_UNHANDLED(handled = false)
174   IPC_END_MESSAGE_MAP()
176   return handled;
179 void AppShimController::OnChannelError() {
180   Close();
183 void AppShimController::OnLaunchAppDone(apps::AppShimLaunchResult result) {
184   if (result != apps::APP_SHIM_LAUNCH_SUCCESS) {
185     Close();
186     return;
187   }
189   launch_app_done_ = true;
192 void AppShimController::Close() {
193   [nsapp_delegate_ terminateNow];
196 void AppShimController::ActivateApp(bool is_reopen) {
197   if (launch_app_done_) {
198     channel_->Send(new AppShimHostMsg_FocusApp(
199         is_reopen ? apps::APP_SHIM_FOCUS_REOPEN : apps::APP_SHIM_FOCUS_NORMAL));
200   }
203 void AppShimController::SendSetAppHidden(bool hidden) {
204   channel_->Send(new AppShimHostMsg_SetAppHidden(hidden));
207 @implementation AppShimDelegate
209 - (id)initWithController:(AppShimController*)controller {
210   if ((self = [super init])) {
211     appShimController_ = controller;
212   }
213   return self;
216 - (BOOL)applicationOpenUntitledFile:(NSApplication *)app {
217   appShimController_->ActivateApp(true);
218   return YES;
221 - (void)applicationWillBecomeActive:(NSNotification*)notification {
222   appShimController_->ActivateApp(false);
225 - (NSApplicationTerminateReply)
226     applicationShouldTerminate:(NSApplication*)sender {
227   if (terminateNow_)
228     return NSTerminateNow;
230   appShimController_->SendQuitApp();
231   // Wait for the channel to close before terminating.
232   terminateRequested_ = YES;
233   return NSTerminateLater;
236 - (void)applicationWillHide:(NSNotification*)notification {
237   appShimController_->SendSetAppHidden(true);
240 - (void)applicationWillUnhide:(NSNotification*)notification {
241   appShimController_->SendSetAppHidden(false);
244 - (void)terminateNow {
245   if (terminateRequested_) {
246     [NSApp replyToApplicationShouldTerminate:NSTerminateNow];
247     return;
248   }
250   terminateNow_ = YES;
251   [NSApp terminate:nil];
254 @end
256 //-----------------------------------------------------------------------------
258 // A ReplyEventHandler is a helper class to send an Apple Event to a process
259 // and call a callback when the reply returns.
261 // This is used to 'ping' the main Chrome process -- once Chrome has sent back
262 // an Apple Event reply, it's guaranteed that it has opened the IPC channel
263 // that the app shim will connect to.
264 @interface ReplyEventHandler : NSObject {
265   base::Callback<void(bool)> onReply_;
266   AEDesc replyEvent_;
268 // Sends an Apple Event to the process identified by |psn|, and calls |replyFn|
269 // when the reply is received. Internally this creates a ReplyEventHandler,
270 // which will delete itself once the reply event has been received.
271 + (void)pingProcess:(const ProcessSerialNumber&)psn
272             andCall:(base::Callback<void(bool)>)replyFn;
273 @end
275 @interface ReplyEventHandler (PrivateMethods)
276 // Initialise the reply event handler. Doesn't register any handlers until
277 // |-pingProcess:| is called. |replyFn| is the function to be called when the
278 // Apple Event reply arrives.
279 - (id)initWithCallback:(base::Callback<void(bool)>)replyFn;
281 // Sends an Apple Event ping to the process identified by |psn| and registers
282 // to listen for a reply.
283 - (void)pingProcess:(const ProcessSerialNumber&)psn;
285 // Called when a response is received from the target process for the ping sent
286 // by |-pingProcess:|.
287 - (void)message:(NSAppleEventDescriptor*)event
288       withReply:(NSAppleEventDescriptor*)reply;
290 // Calls |onReply_|, passing it |success| to specify whether the ping was
291 // successful.
292 - (void)closeWithSuccess:(bool)success;
293 @end
295 @implementation ReplyEventHandler
296 + (void)pingProcess:(const ProcessSerialNumber&)psn
297             andCall:(base::Callback<void(bool)>)replyFn {
298   // The object will release itself when the reply arrives, or possibly earlier
299   // if an unrecoverable error occurs.
300   ReplyEventHandler* handler =
301       [[ReplyEventHandler alloc] initWithCallback:replyFn];
302   [handler pingProcess:psn];
304 @end
306 @implementation ReplyEventHandler (PrivateMethods)
307 - (id)initWithCallback:(base::Callback<void(bool)>)replyFn {
308   if ((self = [super init])) {
309     onReply_ = replyFn;
310   }
311   return self;
314 - (void)pingProcess:(const ProcessSerialNumber&)psn {
315   // Register the reply listener.
316   NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
317   [em setEventHandler:self
318           andSelector:@selector(message:withReply:)
319         forEventClass:'aevt'
320            andEventID:'ansr'];
321   // Craft the Apple Event to send.
322   NSAppleEventDescriptor* target = [NSAppleEventDescriptor
323       descriptorWithDescriptorType:typeProcessSerialNumber
324                              bytes:&psn
325                             length:sizeof(psn)];
326   NSAppleEventDescriptor* initial_event =
327       [NSAppleEventDescriptor
328           appleEventWithEventClass:app_mode::kAEChromeAppClass
329                            eventID:app_mode::kAEChromeAppPing
330                   targetDescriptor:target
331                           returnID:kAutoGenerateReturnID
332                      transactionID:kAnyTransactionID];
333   // And away we go.
334   // TODO(jeremya): if we don't care about the contents of the reply, can we
335   // pass NULL for the reply event parameter?
336   OSStatus status = AESendMessage(
337       [initial_event aeDesc], &replyEvent_, kAEQueueReply, kAEDefaultTimeout);
338   if (status != noErr) {
339     OSSTATUS_LOG(ERROR, status) << "AESendMessage";
340     [self closeWithSuccess:false];
341   }
344 - (void)message:(NSAppleEventDescriptor*)event
345       withReply:(NSAppleEventDescriptor*)reply {
346   [self closeWithSuccess:true];
349 - (void)closeWithSuccess:(bool)success {
350   onReply_.Run(success);
351   NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
352   [em removeEventHandlerForEventClass:'aevt' andEventID:'ansr'];
353   [self release];
355 @end
357 //-----------------------------------------------------------------------------
359 namespace {
361 // Called when the main Chrome process responds to the Apple Event ping that
362 // was sent, or when the ping fails (if |success| is false).
363 void OnPingChromeReply(bool success) {
364   if (!success) {
365     [NSApp terminate:nil];
366     return;
367   }
368   AppShimController* controller = new AppShimController;
369   controller->Init();
372 }  // namespace
374 extern "C" {
376 // |ChromeAppModeStart()| is the point of entry into the framework from the app
377 // mode loader.
378 __attribute__((visibility("default")))
379 int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info);
381 }  // extern "C"
383 int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) {
384   CommandLine::Init(info->argc, info->argv);
386   base::mac::ScopedNSAutoreleasePool scoped_pool;
387   base::AtExitManager exit_manager;
388   chrome::RegisterPathProvider();
390   if (info->major_version < app_mode::kCurrentChromeAppModeInfoMajorVersion) {
391     RAW_LOG(ERROR, "App Mode Loader too old.");
392     return 1;
393   }
394   if (info->major_version > app_mode::kCurrentChromeAppModeInfoMajorVersion) {
395     RAW_LOG(ERROR, "Browser Framework too old to load App Shortcut.");
396     return 1;
397   }
399   g_info = info;
401   // Set bundle paths. This loads the bundles.
402   base::mac::SetOverrideOuterBundlePath(g_info->chrome_outer_bundle_path);
403   base::mac::SetOverrideFrameworkBundlePath(
404       g_info->chrome_versioned_path.Append(chrome::kFrameworkName));
406   // Calculate the preferred locale used by Chrome.
407   // We can't use l10n_util::OverrideLocaleWithCocoaLocale() because it calls
408   // [base::mac::OuterBundle() preferredLocalizations] which gets localizations
409   // from the bundle of the running app (i.e. it is equivalent to
410   // [[NSBundle mainBundle] preferredLocalizations]) instead of the target
411   // bundle.
412   NSArray* preferred_languages = [NSLocale preferredLanguages];
413   NSArray* supported_languages = [base::mac::OuterBundle() localizations];
414   std::string preferred_localization;
415   for (NSString* language in preferred_languages) {
416     if ([supported_languages containsObject:language]) {
417       preferred_localization = base::SysNSStringToUTF8(language);
418       break;
419     }
420   }
421   std::string locale = l10n_util::NormalizeLocale(
422       l10n_util::GetApplicationLocale(preferred_localization));
424   // Load localized strings.
425   ResourceBundle::InitSharedInstanceLocaleOnly(locale, NULL);
427   // Launch the IO thread.
428   base::Thread::Options io_thread_options;
429   io_thread_options.message_loop_type = base::MessageLoop::TYPE_IO;
430   base::Thread *io_thread = new base::Thread("CrAppShimIO");
431   io_thread->StartWithOptions(io_thread_options);
432   g_io_thread = io_thread;
434   // Find already running instances of Chrome.
435   NSString* chrome_bundle_id = [base::mac::OuterBundle() bundleIdentifier];
436   NSArray* existing_chrome = [NSRunningApplication
437       runningApplicationsWithBundleIdentifier:chrome_bundle_id];
439   // Launch Chrome if it isn't already running.
440   ProcessSerialNumber psn;
441   if ([existing_chrome count] > 0) {
442     OSStatus status = GetProcessForPID(
443         [[existing_chrome objectAtIndex:0] processIdentifier], &psn);
444     if (status)
445       return 1;
447   } else {
448     CommandLine command_line(CommandLine::NO_PROGRAM);
449     command_line.AppendSwitch(switches::kSilentLaunch);
450     command_line.AppendSwitchPath(switches::kProfileDirectory,
451                                   info->profile_dir);
452     bool success =
453         base::mac::OpenApplicationWithPath(base::mac::OuterBundlePath(),
454                                            command_line,
455                                            kLSLaunchDefaults,
456                                            &psn);
457     if (!success)
458       return 1;
459   }
461   // This code abuses the fact that Apple Events sent before the process is
462   // fully initialized don't receive a reply until its run loop starts. Once
463   // the reply is received, Chrome will have opened its IPC port, guaranteed.
464   [ReplyEventHandler pingProcess:psn andCall:base::Bind(&OnPingChromeReply)];
466   base::MessageLoopForUI main_message_loop;
467   main_message_loop.set_thread_name("MainThread");
468   base::PlatformThread::SetName("CrAppShimMain");
469   main_message_loop.Run();
470   return 0;