- add sources.
[platform/framework/web/crosswalk.git] / src / apps / app_shim / chrome_main_app_mode_mac.mm
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.
4
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.
9
10 #import <Cocoa/Cocoa.h>
11 #include <vector>
12
13 #include "apps/app_shim/app_shim_messages.h"
14 #include "base/at_exit.h"
15 #include "base/command_line.h"
16 #include "base/files/file_path.h"
17 #include "base/logging.h"
18 #include "base/mac/bundle_locations.h"
19 #include "base/mac/foundation_util.h"
20 #include "base/mac/launch_services_util.h"
21 #include "base/mac/mac_logging.h"
22 #include "base/mac/mac_util.h"
23 #include "base/mac/scoped_nsautorelease_pool.h"
24 #include "base/mac/scoped_nsobject.h"
25 #include "base/message_loop/message_loop.h"
26 #include "base/path_service.h"
27 #include "base/strings/string_number_conversions.h"
28 #include "base/strings/sys_string_conversions.h"
29 #include "base/threading/thread.h"
30 #include "chrome/common/chrome_constants.h"
31 #include "chrome/common/chrome_paths.h"
32 #include "chrome/common/chrome_switches.h"
33 #include "chrome/common/mac/app_mode_common.h"
34 #include "grit/generated_resources.h"
35 #include "ipc/ipc_channel_proxy.h"
36 #include "ipc/ipc_listener.h"
37 #include "ipc/ipc_message.h"
38 #include "ui/base/resource/resource_bundle.h"
39 #include "ui/base/l10n/l10n_util.h"
40
41 // Replicate specific 10.7 SDK declarations for building with prior SDKs.
42 #if !defined(MAC_OS_X_VERSION_10_7) || \
43     MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
44
45 @interface NSApplication (LionSDKDeclarations)
46 - (void)disableRelaunchOnLogin;
47 @end
48
49 #endif  // MAC_OS_X_VERSION_10_7
50
51 namespace {
52
53 const app_mode::ChromeAppModeInfo* g_info;
54 base::Thread* g_io_thread = NULL;
55
56 }  // namespace
57
58 class AppShimController;
59
60 // An application delegate to catch user interactions and send the appropriate
61 // IPC messages to Chrome.
62 @interface AppShimDelegate : NSObject<NSApplicationDelegate> {
63  @private
64   AppShimController* appShimController_;  // Weak, initially NULL.
65   BOOL terminateNow_;
66   BOOL terminateRequested_;
67   std::vector<base::FilePath> filesToOpenAtStartup_;
68 }
69
70 // The controller is initially NULL. Setting it indicates to the delegate that
71 // the controller has finished initialization.
72 - (void)setController:(AppShimController*)controller;
73
74 // Gets files that were queued because the controller was not ready.
75 // Returns whether any FilePaths were added to |out|.
76 - (BOOL)getFilesToOpenAtStartup:(std::vector<base::FilePath>*)out;
77
78 // If the controller is ready, this sends a FocusApp with the files to open.
79 // Otherwise, this adds the files to |filesToOpenAtStartup_|.
80 // Takes an array of NSString*.
81 - (void)openFiles:(NSArray*)filename;
82
83 // Terminate immediately. This is necessary as we override terminate: to send
84 // a QuitApp message.
85 - (void)terminateNow;
86
87 @end
88
89 // The AppShimController is responsible for communication with the main Chrome
90 // process, and generally controls the lifetime of the app shim process.
91 class AppShimController : public IPC::Listener {
92  public:
93   AppShimController();
94   virtual ~AppShimController();
95
96   // Called when the main Chrome process responds to the Apple Event ping that
97   // was sent, or when the ping fails (if |success| is false).
98   void OnPingChromeReply(bool success);
99
100   // Connects to Chrome and sends a LaunchApp message.
101   void Init();
102
103   // Builds main menu bar items.
104   void SetUpMenu();
105
106   void SendSetAppHidden(bool hidden);
107
108   void SendQuitApp();
109
110   // Called when the app is activated, e.g. by clicking on it in the dock, by
111   // dropping a file on the dock icon, or by Cmd+Tabbing to it.
112   // Returns whether the message was sent.
113   bool SendFocusApp(apps::AppShimFocusType focus_type,
114                     const std::vector<base::FilePath>& files);
115
116  private:
117   // IPC::Listener implemetation.
118   virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE;
119   virtual void OnChannelError() OVERRIDE;
120
121   // If Chrome failed to launch the app, |success| will be false and the app
122   // shim process should die.
123   void OnLaunchAppDone(apps::AppShimLaunchResult result);
124
125   // Hide this app.
126   void OnHide();
127
128   // Requests user attention.
129   void OnRequestUserAttention();
130
131   // Terminates the app shim process.
132   void Close();
133
134   IPC::ChannelProxy* channel_;
135   base::scoped_nsobject<AppShimDelegate> delegate_;
136   bool launch_app_done_;
137
138   DISALLOW_COPY_AND_ASSIGN(AppShimController);
139 };
140
141 AppShimController::AppShimController()
142     : channel_(NULL),
143       delegate_([[AppShimDelegate alloc] init]),
144       launch_app_done_(false) {
145   // Since AppShimController is created before the main message loop starts,
146   // NSApp will not be set, so use sharedApplication.
147   [[NSApplication sharedApplication] setDelegate:delegate_];
148 }
149
150 AppShimController::~AppShimController() {
151   // Un-set the delegate since NSApplication does not retain it.
152   [NSApp setDelegate:nil];
153 }
154
155 void AppShimController::OnPingChromeReply(bool success) {
156   if (!success) {
157     [NSApp terminate:nil];
158     return;
159   }
160
161   Init();
162 }
163
164 void AppShimController::Init() {
165   DCHECK(g_io_thread);
166
167   SetUpMenu();
168
169   // Chrome will relaunch shims when relaunching apps.
170   if (base::mac::IsOSLionOrLater())
171     [NSApp disableRelaunchOnLogin];
172
173   // The user_data_dir for shims actually contains the app_data_path.
174   // I.e. <user_data_dir>/<profile_dir>/Web Applications/_crx_extensionid/
175   base::FilePath user_data_dir =
176       g_info->user_data_dir.DirName().DirName().DirName();
177   CHECK(!user_data_dir.empty());
178
179   base::FilePath socket_path =
180       user_data_dir.Append(app_mode::kAppShimSocketName);
181   IPC::ChannelHandle handle(socket_path.value());
182   channel_ = new IPC::ChannelProxy(handle, IPC::Channel::MODE_NAMED_CLIENT,
183       this, g_io_thread->message_loop_proxy().get());
184
185   bool launched_by_chrome =
186       CommandLine::ForCurrentProcess()->HasSwitch(
187           app_mode::kLaunchedByChromeProcessId);
188   apps::AppShimLaunchType launch_type = launched_by_chrome ?
189           apps::APP_SHIM_LAUNCH_REGISTER_ONLY : apps::APP_SHIM_LAUNCH_NORMAL;
190
191   [delegate_ setController:this];
192
193   std::vector<base::FilePath> files;
194   [delegate_ getFilesToOpenAtStartup:&files];
195
196   channel_->Send(new AppShimHostMsg_LaunchApp(
197       g_info->profile_dir, g_info->app_mode_id, launch_type, files));
198 }
199
200 void AppShimController::SetUpMenu() {
201   NSString* title = base::SysUTF16ToNSString(g_info->app_mode_name);
202
203   // Create a main menu since [NSApp mainMenu] is nil.
204   base::scoped_nsobject<NSMenu> main_menu([[NSMenu alloc] initWithTitle:title]);
205
206   // The title of the first item is replaced by OSX with the name of the app and
207   // bold styling. Create a dummy item for this and make it hidden.
208   NSMenuItem* dummy_item = [main_menu addItemWithTitle:title
209                                                 action:nil
210                                          keyEquivalent:@""];
211   base::scoped_nsobject<NSMenu> dummy_submenu(
212       [[NSMenu alloc] initWithTitle:title]);
213   [dummy_item setSubmenu:dummy_submenu];
214   [dummy_item setHidden:YES];
215
216   // Construct an unbolded app menu, to match how it appears in the Chrome menu
217   // bar when the app is focused.
218   NSMenuItem* item = [main_menu addItemWithTitle:title
219                                           action:nil
220                                    keyEquivalent:@""];
221   base::scoped_nsobject<NSMenu> submenu([[NSMenu alloc] initWithTitle:title]);
222   [item setSubmenu:submenu];
223
224   // Add a quit entry.
225   NSString* quit_localized_string =
226       l10n_util::GetNSStringF(IDS_EXIT_MAC, g_info->app_mode_name);
227   [submenu addItemWithTitle:quit_localized_string
228                      action:@selector(terminate:)
229               keyEquivalent:@"q"];
230
231   // Add File, Edit, and Window menus. These are just here to make the
232   // transition smoother, i.e. from another application to the shim then to
233   // Chrome.
234   [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_FILE_MENU_MAC)
235                        action:nil
236                 keyEquivalent:@""];
237   [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_EDIT_MENU_MAC)
238                        action:nil
239                 keyEquivalent:@""];
240   [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_WINDOW_MENU_MAC)
241                        action:nil
242                 keyEquivalent:@""];
243
244   [NSApp setMainMenu:main_menu];
245 }
246
247 void AppShimController::SendQuitApp() {
248   channel_->Send(new AppShimHostMsg_QuitApp);
249 }
250
251 bool AppShimController::OnMessageReceived(const IPC::Message& message) {
252   bool handled = true;
253   IPC_BEGIN_MESSAGE_MAP(AppShimController, message)
254     IPC_MESSAGE_HANDLER(AppShimMsg_LaunchApp_Done, OnLaunchAppDone)
255     IPC_MESSAGE_HANDLER(AppShimMsg_Hide, OnHide)
256     IPC_MESSAGE_HANDLER(AppShimMsg_RequestUserAttention, OnRequestUserAttention)
257     IPC_MESSAGE_UNHANDLED(handled = false)
258   IPC_END_MESSAGE_MAP()
259
260   return handled;
261 }
262
263 void AppShimController::OnChannelError() {
264   Close();
265 }
266
267 void AppShimController::OnLaunchAppDone(apps::AppShimLaunchResult result) {
268   if (result != apps::APP_SHIM_LAUNCH_SUCCESS) {
269     Close();
270     return;
271   }
272
273   std::vector<base::FilePath> files;
274   if ([delegate_ getFilesToOpenAtStartup:&files])
275     SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES, files);
276
277   launch_app_done_ = true;
278 }
279
280 void AppShimController::OnHide() {
281   [NSApp hide:nil];
282 }
283
284 void AppShimController::OnRequestUserAttention() {
285   [NSApp requestUserAttention:NSInformationalRequest];
286 }
287
288 void AppShimController::Close() {
289   [delegate_ terminateNow];
290 }
291
292 bool AppShimController::SendFocusApp(apps::AppShimFocusType focus_type,
293                                      const std::vector<base::FilePath>& files) {
294   if (launch_app_done_) {
295     channel_->Send(new AppShimHostMsg_FocusApp(focus_type, files));
296     return true;
297   }
298
299   return false;
300 }
301
302 void AppShimController::SendSetAppHidden(bool hidden) {
303   channel_->Send(new AppShimHostMsg_SetAppHidden(hidden));
304 }
305
306 @implementation AppShimDelegate
307
308 - (BOOL)getFilesToOpenAtStartup:(std::vector<base::FilePath>*)out {
309   if (filesToOpenAtStartup_.empty())
310     return NO;
311
312   out->insert(out->end(),
313               filesToOpenAtStartup_.begin(),
314               filesToOpenAtStartup_.end());
315   filesToOpenAtStartup_.clear();
316   return YES;
317 }
318
319 - (void)setController:(AppShimController*)controller {
320   appShimController_ = controller;
321 }
322
323 - (void)openFiles:(NSArray*)filenames {
324   std::vector<base::FilePath> filePaths;
325   for (NSString* filename in filenames)
326     filePaths.push_back(base::mac::NSStringToFilePath(filename));
327
328   // If the AppShimController is ready, try to send a FocusApp. If that fails,
329   // (e.g. if launching has not finished), enqueue the files.
330   if (appShimController_ &&
331       appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES,
332                                        filePaths)) {
333     return;
334   }
335
336   filesToOpenAtStartup_.insert(filesToOpenAtStartup_.end(),
337                                filePaths.begin(),
338                                filePaths.end());
339 }
340
341 - (BOOL)application:(NSApplication*)app
342            openFile:(NSString*)filename {
343   [self openFiles:@[filename]];
344   return YES;
345 }
346
347 - (void)application:(NSApplication*)app
348           openFiles:(NSArray*)filenames {
349   [self openFiles:filenames];
350   [app replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
351 }
352
353 - (BOOL)applicationOpenUntitledFile:(NSApplication*)app {
354   if (appShimController_) {
355     return appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_REOPEN,
356                                             std::vector<base::FilePath>());
357   }
358
359   return NO;
360 }
361
362 - (void)applicationWillBecomeActive:(NSNotification*)notification {
363   if (appShimController_) {
364     appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_NORMAL,
365                                      std::vector<base::FilePath>());
366   }
367 }
368
369 - (NSApplicationTerminateReply)
370     applicationShouldTerminate:(NSApplication*)sender {
371   if (terminateNow_ || !appShimController_)
372     return NSTerminateNow;
373
374   appShimController_->SendQuitApp();
375   // Wait for the channel to close before terminating.
376   terminateRequested_ = YES;
377   return NSTerminateLater;
378 }
379
380 - (void)applicationWillHide:(NSNotification*)notification {
381   if (appShimController_)
382     appShimController_->SendSetAppHidden(true);
383 }
384
385 - (void)applicationWillUnhide:(NSNotification*)notification {
386   if (appShimController_)
387     appShimController_->SendSetAppHidden(false);
388 }
389
390 - (void)terminateNow {
391   if (terminateRequested_) {
392     [NSApp replyToApplicationShouldTerminate:NSTerminateNow];
393     return;
394   }
395
396   terminateNow_ = YES;
397   [NSApp terminate:nil];
398 }
399
400 @end
401
402 //-----------------------------------------------------------------------------
403
404 // A ReplyEventHandler is a helper class to send an Apple Event to a process
405 // and call a callback when the reply returns.
406 //
407 // This is used to 'ping' the main Chrome process -- once Chrome has sent back
408 // an Apple Event reply, it's guaranteed that it has opened the IPC channel
409 // that the app shim will connect to.
410 @interface ReplyEventHandler : NSObject {
411   base::Callback<void(bool)> onReply_;
412   AEDesc replyEvent_;
413 }
414 // Sends an Apple Event to the process identified by |psn|, and calls |replyFn|
415 // when the reply is received. Internally this creates a ReplyEventHandler,
416 // which will delete itself once the reply event has been received.
417 + (void)pingProcess:(const ProcessSerialNumber&)psn
418             andCall:(base::Callback<void(bool)>)replyFn;
419 @end
420
421 @interface ReplyEventHandler (PrivateMethods)
422 // Initialise the reply event handler. Doesn't register any handlers until
423 // |-pingProcess:| is called. |replyFn| is the function to be called when the
424 // Apple Event reply arrives.
425 - (id)initWithCallback:(base::Callback<void(bool)>)replyFn;
426
427 // Sends an Apple Event ping to the process identified by |psn| and registers
428 // to listen for a reply.
429 - (void)pingProcess:(const ProcessSerialNumber&)psn;
430
431 // Called when a response is received from the target process for the ping sent
432 // by |-pingProcess:|.
433 - (void)message:(NSAppleEventDescriptor*)event
434       withReply:(NSAppleEventDescriptor*)reply;
435
436 // Calls |onReply_|, passing it |success| to specify whether the ping was
437 // successful.
438 - (void)closeWithSuccess:(bool)success;
439 @end
440
441 @implementation ReplyEventHandler
442 + (void)pingProcess:(const ProcessSerialNumber&)psn
443             andCall:(base::Callback<void(bool)>)replyFn {
444   // The object will release itself when the reply arrives, or possibly earlier
445   // if an unrecoverable error occurs.
446   ReplyEventHandler* handler =
447       [[ReplyEventHandler alloc] initWithCallback:replyFn];
448   [handler pingProcess:psn];
449 }
450 @end
451
452 @implementation ReplyEventHandler (PrivateMethods)
453 - (id)initWithCallback:(base::Callback<void(bool)>)replyFn {
454   if ((self = [super init])) {
455     onReply_ = replyFn;
456   }
457   return self;
458 }
459
460 - (void)pingProcess:(const ProcessSerialNumber&)psn {
461   // Register the reply listener.
462   NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
463   [em setEventHandler:self
464           andSelector:@selector(message:withReply:)
465         forEventClass:'aevt'
466            andEventID:'ansr'];
467   // Craft the Apple Event to send.
468   NSAppleEventDescriptor* target = [NSAppleEventDescriptor
469       descriptorWithDescriptorType:typeProcessSerialNumber
470                              bytes:&psn
471                             length:sizeof(psn)];
472   NSAppleEventDescriptor* initial_event =
473       [NSAppleEventDescriptor
474           appleEventWithEventClass:app_mode::kAEChromeAppClass
475                            eventID:app_mode::kAEChromeAppPing
476                   targetDescriptor:target
477                           returnID:kAutoGenerateReturnID
478                      transactionID:kAnyTransactionID];
479   // And away we go.
480   // TODO(jeremya): if we don't care about the contents of the reply, can we
481   // pass NULL for the reply event parameter?
482   OSStatus status = AESendMessage(
483       [initial_event aeDesc], &replyEvent_, kAEQueueReply, kAEDefaultTimeout);
484   if (status != noErr) {
485     OSSTATUS_LOG(ERROR, status) << "AESendMessage";
486     [self closeWithSuccess:false];
487   }
488 }
489
490 - (void)message:(NSAppleEventDescriptor*)event
491       withReply:(NSAppleEventDescriptor*)reply {
492   [self closeWithSuccess:true];
493 }
494
495 - (void)closeWithSuccess:(bool)success {
496   onReply_.Run(success);
497   NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
498   [em removeEventHandlerForEventClass:'aevt' andEventID:'ansr'];
499   [self release];
500 }
501 @end
502
503 //-----------------------------------------------------------------------------
504
505 extern "C" {
506
507 // |ChromeAppModeStart()| is the point of entry into the framework from the app
508 // mode loader.
509 __attribute__((visibility("default")))
510 int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info);
511
512 }  // extern "C"
513
514 int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) {
515   CommandLine::Init(info->argc, info->argv);
516
517   base::mac::ScopedNSAutoreleasePool scoped_pool;
518   base::AtExitManager exit_manager;
519   chrome::RegisterPathProvider();
520
521   if (info->major_version < app_mode::kCurrentChromeAppModeInfoMajorVersion) {
522     RAW_LOG(ERROR, "App Mode Loader too old.");
523     return 1;
524   }
525   if (info->major_version > app_mode::kCurrentChromeAppModeInfoMajorVersion) {
526     RAW_LOG(ERROR, "Browser Framework too old to load App Shortcut.");
527     return 1;
528   }
529
530   g_info = info;
531
532   // Set bundle paths. This loads the bundles.
533   base::mac::SetOverrideOuterBundlePath(g_info->chrome_outer_bundle_path);
534   base::mac::SetOverrideFrameworkBundlePath(
535       g_info->chrome_versioned_path.Append(chrome::kFrameworkName));
536
537   // Calculate the preferred locale used by Chrome.
538   // We can't use l10n_util::OverrideLocaleWithCocoaLocale() because it calls
539   // [base::mac::OuterBundle() preferredLocalizations] which gets localizations
540   // from the bundle of the running app (i.e. it is equivalent to
541   // [[NSBundle mainBundle] preferredLocalizations]) instead of the target
542   // bundle.
543   NSArray* preferred_languages = [NSLocale preferredLanguages];
544   NSArray* supported_languages = [base::mac::OuterBundle() localizations];
545   std::string preferred_localization;
546   for (NSString* language in preferred_languages) {
547     if ([supported_languages containsObject:language]) {
548       preferred_localization = base::SysNSStringToUTF8(language);
549       break;
550     }
551   }
552   std::string locale = l10n_util::NormalizeLocale(
553       l10n_util::GetApplicationLocale(preferred_localization));
554
555   // Load localized strings.
556   ResourceBundle::InitSharedInstanceLocaleOnly(locale, NULL);
557
558   // Launch the IO thread.
559   base::Thread::Options io_thread_options;
560   io_thread_options.message_loop_type = base::MessageLoop::TYPE_IO;
561   base::Thread *io_thread = new base::Thread("CrAppShimIO");
562   io_thread->StartWithOptions(io_thread_options);
563   g_io_thread = io_thread;
564
565   // Find already running instances of Chrome.
566   pid_t pid = -1;
567   std::string chrome_process_id = CommandLine::ForCurrentProcess()->
568       GetSwitchValueASCII(app_mode::kLaunchedByChromeProcessId);
569   if (!chrome_process_id.empty()) {
570     if (!base::StringToInt(chrome_process_id, &pid))
571       LOG(FATAL) << "Invalid PID: " << chrome_process_id;
572   } else {
573     NSString* chrome_bundle_id = [base::mac::OuterBundle() bundleIdentifier];
574     NSArray* existing_chrome = [NSRunningApplication
575         runningApplicationsWithBundleIdentifier:chrome_bundle_id];
576     if ([existing_chrome count] > 0)
577       pid = [[existing_chrome objectAtIndex:0] processIdentifier];
578   }
579
580   // Launch Chrome if it isn't already running.
581   ProcessSerialNumber psn;
582   if (pid > -1) {
583     OSStatus status = GetProcessForPID(pid, &psn);
584     if (status)
585       return 1;
586
587   } else {
588     CommandLine command_line(CommandLine::NO_PROGRAM);
589     command_line.AppendSwitch(switches::kSilentLaunch);
590     command_line.AppendSwitchPath(switches::kProfileDirectory,
591                                   info->profile_dir);
592     bool success =
593         base::mac::OpenApplicationWithPath(base::mac::OuterBundlePath(),
594                                            command_line,
595                                            kLSLaunchDefaults,
596                                            &psn);
597     if (!success)
598       return 1;
599   }
600
601   AppShimController controller;
602   base::Callback<void(bool)> on_ping_chrome_reply =
603       base::Bind(&AppShimController::OnPingChromeReply,
604                  base::Unretained(&controller));
605
606   // This code abuses the fact that Apple Events sent before the process is
607   // fully initialized don't receive a reply until its run loop starts. Once
608   // the reply is received, Chrome will have opened its IPC port, guaranteed.
609   [ReplyEventHandler pingProcess:psn
610                          andCall:on_ping_chrome_reply];
611
612   base::MessageLoopForUI main_message_loop;
613   main_message_loop.set_thread_name("MainThread");
614   base::PlatformThread::SetName("CrAppShimMain");
615   main_message_loop.Run();
616   return 0;
617 }