browser_mac.mm 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. // Copyright (c) 2013 GitHub, Inc.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #include "shell/browser/browser.h"
  5. #include <memory>
  6. #include <string>
  7. #include <utility>
  8. #include "base/apple/bridging.h"
  9. #include "base/apple/bundle_locations.h"
  10. #include "base/apple/scoped_cftyperef.h"
  11. #include "base/i18n/rtl.h"
  12. #include "base/mac/mac_util.h"
  13. #include "base/mac/mac_util.mm"
  14. #include "base/strings/string_number_conversions.h"
  15. #include "base/strings/sys_string_conversions.h"
  16. #include "chrome/browser/browser_process.h"
  17. #include "net/base/mac/url_conversions.h"
  18. #include "shell/browser/badging/badge_manager.h"
  19. #include "shell/browser/mac/dict_util.h"
  20. #include "shell/browser/mac/electron_application.h"
  21. #include "shell/browser/mac/electron_application_delegate.h"
  22. #include "shell/browser/native_window.h"
  23. #include "shell/browser/window_list.h"
  24. #include "shell/common/api/electron_api_native_image.h"
  25. #include "shell/common/application_info.h"
  26. #include "shell/common/gin_converters/image_converter.h"
  27. #include "shell/common/gin_helper/arguments.h"
  28. #include "shell/common/gin_helper/dictionary.h"
  29. #include "shell/common/gin_helper/error_thrower.h"
  30. #include "shell/common/gin_helper/promise.h"
  31. #include "shell/common/platform_util.h"
  32. #include "ui/base/resource/resource_scale_factor.h"
  33. #include "ui/gfx/image/image.h"
  34. #include "url/gurl.h"
  35. namespace electron {
  36. namespace {
  37. NSString* GetAppPathForProtocol(const GURL& url) {
  38. NSURL* ns_url = [NSURL
  39. URLWithString:base::SysUTF8ToNSString(url.possibly_invalid_spec())];
  40. base::apple::ScopedCFTypeRef<CFErrorRef> out_err;
  41. base::apple::ScopedCFTypeRef<CFURLRef> openingApp(
  42. LSCopyDefaultApplicationURLForURL(base::apple::NSToCFPtrCast(ns_url),
  43. kLSRolesAll, out_err.InitializeInto()));
  44. if (out_err) {
  45. // likely kLSApplicationNotFoundErr
  46. return nullptr;
  47. }
  48. NSString* app_path = [base::apple::CFToNSPtrCast(openingApp.get()) path];
  49. return app_path;
  50. }
  51. gfx::Image GetApplicationIconForProtocol(NSString* _Nonnull app_path) {
  52. NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile:app_path];
  53. gfx::Image icon(image);
  54. return icon;
  55. }
  56. std::u16string GetAppDisplayNameForProtocol(NSString* app_path) {
  57. NSString* app_display_name =
  58. [[NSFileManager defaultManager] displayNameAtPath:app_path];
  59. return base::SysNSStringToUTF16(app_display_name);
  60. }
  61. #if !IS_MAS_BUILD()
  62. bool CheckLoginItemStatus(bool* is_hidden) {
  63. base::mac::LoginItemsFileList login_items;
  64. if (!login_items.Initialize())
  65. return false;
  66. base::apple::ScopedCFTypeRef<LSSharedFileListItemRef> item(
  67. login_items.GetLoginItemForMainApp());
  68. if (!item.get())
  69. return false;
  70. if (is_hidden)
  71. *is_hidden = base::mac::IsHiddenLoginItem(item);
  72. return true;
  73. }
  74. #endif
  75. } // namespace
  76. v8::Local<v8::Promise> Browser::GetApplicationInfoForProtocol(
  77. v8::Isolate* isolate,
  78. const GURL& url) {
  79. gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
  80. v8::Local<v8::Promise> handle = promise.GetHandle();
  81. auto dict = gin_helper::Dictionary::CreateEmpty(isolate);
  82. NSString* ns_app_path = GetAppPathForProtocol(url);
  83. if (!ns_app_path) {
  84. promise.RejectWithErrorMessage(
  85. "Unable to retrieve installation path to app");
  86. return handle;
  87. }
  88. std::u16string app_path = base::SysNSStringToUTF16(ns_app_path);
  89. std::u16string app_display_name = GetAppDisplayNameForProtocol(ns_app_path);
  90. gfx::Image app_icon = GetApplicationIconForProtocol(ns_app_path);
  91. dict.Set("name", app_display_name);
  92. dict.Set("path", app_path);
  93. dict.Set("icon", app_icon);
  94. promise.Resolve(dict);
  95. return handle;
  96. }
  97. void Browser::SetShutdownHandler(base::RepeatingCallback<bool()> handler) {
  98. [[AtomApplication sharedApplication] setShutdownHandler:std::move(handler)];
  99. }
  100. void Browser::Focus(gin::Arguments* args) {
  101. gin_helper::Dictionary opts;
  102. bool steal_focus = false;
  103. if (args->GetNext(&opts)) {
  104. gin_helper::ErrorThrower thrower(args->isolate());
  105. if (!opts.Get("steal", &steal_focus)) {
  106. thrower.ThrowError(
  107. "Expected options object to contain a 'steal' boolean property");
  108. return;
  109. }
  110. }
  111. [[AtomApplication sharedApplication] activateIgnoringOtherApps:steal_focus];
  112. }
  113. void Browser::Hide() {
  114. [[AtomApplication sharedApplication] hide:nil];
  115. }
  116. bool Browser::IsHidden() {
  117. return [[AtomApplication sharedApplication] isHidden];
  118. }
  119. void Browser::Show() {
  120. [[AtomApplication sharedApplication] unhide:nil];
  121. }
  122. void Browser::AddRecentDocument(const base::FilePath& path) {
  123. NSString* path_string = base::apple::FilePathToNSString(path);
  124. if (!path_string)
  125. return;
  126. NSURL* u = [NSURL fileURLWithPath:path_string];
  127. if (!u)
  128. return;
  129. [[NSDocumentController sharedDocumentController] noteNewRecentDocumentURL:u];
  130. }
  131. void Browser::ClearRecentDocuments() {
  132. [[NSDocumentController sharedDocumentController] clearRecentDocuments:nil];
  133. }
  134. bool Browser::RemoveAsDefaultProtocolClient(const std::string& protocol,
  135. gin::Arguments* args) {
  136. NSString* identifier = [base::apple::MainBundle() bundleIdentifier];
  137. if (!identifier)
  138. return false;
  139. if (!Browser::IsDefaultProtocolClient(protocol, args))
  140. return false;
  141. NSString* protocol_ns = [NSString stringWithUTF8String:protocol.c_str()];
  142. CFStringRef protocol_cf = base::apple::NSToCFPtrCast(protocol_ns);
  143. // TODO(codebytere): Use -[NSWorkspace URLForApplicationToOpenURL:] instead
  144. #pragma clang diagnostic push
  145. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  146. CFArrayRef bundleList = LSCopyAllHandlersForURLScheme(protocol_cf);
  147. #pragma clang diagnostic pop
  148. if (!bundleList) {
  149. return false;
  150. }
  151. // On macOS, we can't query the default, but the handlers list seems to put
  152. // Apple's defaults first, so we'll use the first option that isn't our bundle
  153. CFStringRef other = nil;
  154. for (CFIndex i = 0; i < CFArrayGetCount(bundleList); ++i) {
  155. other =
  156. base::apple::CFCast<CFStringRef>(CFArrayGetValueAtIndex(bundleList, i));
  157. if (![identifier isEqualToString:(__bridge NSString*)other]) {
  158. break;
  159. }
  160. }
  161. // No other app was found set it to none instead of setting it back to itself.
  162. if ([identifier isEqualToString:(__bridge NSString*)other]) {
  163. other = base::apple::NSToCFPtrCast(@"None");
  164. }
  165. OSStatus return_code = LSSetDefaultHandlerForURLScheme(protocol_cf, other);
  166. return return_code == noErr;
  167. }
  168. bool Browser::SetAsDefaultProtocolClient(const std::string& protocol,
  169. gin::Arguments* args) {
  170. if (protocol.empty())
  171. return false;
  172. NSString* identifier = [base::apple::MainBundle() bundleIdentifier];
  173. if (!identifier)
  174. return false;
  175. NSString* protocol_ns = [NSString stringWithUTF8String:protocol.c_str()];
  176. OSStatus return_code =
  177. LSSetDefaultHandlerForURLScheme(base::apple::NSToCFPtrCast(protocol_ns),
  178. base::apple::NSToCFPtrCast(identifier));
  179. return return_code == noErr;
  180. }
  181. bool Browser::IsDefaultProtocolClient(const std::string& protocol,
  182. gin::Arguments* args) {
  183. if (protocol.empty())
  184. return false;
  185. NSString* identifier = [base::apple::MainBundle() bundleIdentifier];
  186. if (!identifier)
  187. return false;
  188. NSString* protocol_ns = [NSString stringWithUTF8String:protocol.c_str()];
  189. // TODO(codebytere): Use -[NSWorkspace URLForApplicationToOpenURL:] instead
  190. #pragma clang diagnostic push
  191. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  192. base::apple::ScopedCFTypeRef<CFStringRef> bundleId(
  193. LSCopyDefaultHandlerForURLScheme(
  194. base::apple::NSToCFPtrCast(protocol_ns)));
  195. #pragma clang diagnostic pop
  196. if (!bundleId)
  197. return false;
  198. // Ensure the comparison is case-insensitive
  199. // as LS does not persist the case of the bundle id.
  200. NSComparisonResult result =
  201. [base::apple::CFToNSPtrCast(bundleId) caseInsensitiveCompare:identifier];
  202. return result == NSOrderedSame;
  203. }
  204. std::u16string Browser::GetApplicationNameForProtocol(const GURL& url) {
  205. NSString* app_path = GetAppPathForProtocol(url);
  206. if (!app_path) {
  207. return std::u16string();
  208. }
  209. std::u16string app_display_name = GetAppDisplayNameForProtocol(app_path);
  210. return app_display_name;
  211. }
  212. bool Browser::SetBadgeCount(absl::optional<int> count) {
  213. DockSetBadgeText(!count.has_value() || count.value() != 0
  214. ? badging::BadgeManager::GetBadgeString(count)
  215. : "");
  216. if (count.has_value()) {
  217. badge_count_ = count.value();
  218. } else {
  219. badge_count_ = 0;
  220. }
  221. return true;
  222. }
  223. void Browser::SetUserActivity(const std::string& type,
  224. base::Value::Dict user_info,
  225. gin::Arguments* args) {
  226. std::string url_string;
  227. args->GetNext(&url_string);
  228. [[AtomApplication sharedApplication]
  229. setCurrentActivity:base::SysUTF8ToNSString(type)
  230. withUserInfo:DictionaryValueToNSDictionary(std::move(user_info))
  231. withWebpageURL:net::NSURLWithGURL(GURL(url_string))];
  232. }
  233. std::string Browser::GetCurrentActivityType() {
  234. NSUserActivity* userActivity =
  235. [[AtomApplication sharedApplication] getCurrentActivity];
  236. return base::SysNSStringToUTF8(userActivity.activityType);
  237. }
  238. void Browser::InvalidateCurrentActivity() {
  239. [[AtomApplication sharedApplication] invalidateCurrentActivity];
  240. }
  241. void Browser::ResignCurrentActivity() {
  242. [[AtomApplication sharedApplication] resignCurrentActivity];
  243. }
  244. void Browser::UpdateCurrentActivity(const std::string& type,
  245. base::Value::Dict user_info) {
  246. [[AtomApplication sharedApplication]
  247. updateCurrentActivity:base::SysUTF8ToNSString(type)
  248. withUserInfo:DictionaryValueToNSDictionary(
  249. std::move(user_info))];
  250. }
  251. bool Browser::WillContinueUserActivity(const std::string& type) {
  252. bool prevent_default = false;
  253. for (BrowserObserver& observer : observers_)
  254. observer.OnWillContinueUserActivity(&prevent_default, type);
  255. return prevent_default;
  256. }
  257. void Browser::DidFailToContinueUserActivity(const std::string& type,
  258. const std::string& error) {
  259. for (BrowserObserver& observer : observers_)
  260. observer.OnDidFailToContinueUserActivity(type, error);
  261. }
  262. bool Browser::ContinueUserActivity(const std::string& type,
  263. base::Value::Dict user_info,
  264. base::Value::Dict details) {
  265. bool prevent_default = false;
  266. for (BrowserObserver& observer : observers_)
  267. observer.OnContinueUserActivity(&prevent_default, type, user_info.Clone(),
  268. details.Clone());
  269. return prevent_default;
  270. }
  271. void Browser::UserActivityWasContinued(const std::string& type,
  272. base::Value::Dict user_info) {
  273. for (BrowserObserver& observer : observers_)
  274. observer.OnUserActivityWasContinued(type, user_info.Clone());
  275. }
  276. bool Browser::UpdateUserActivityState(const std::string& type,
  277. base::Value::Dict user_info) {
  278. bool prevent_default = false;
  279. for (BrowserObserver& observer : observers_)
  280. observer.OnUpdateUserActivityState(&prevent_default, type,
  281. user_info.Clone());
  282. return prevent_default;
  283. }
  284. // Modified from chrome/browser/ui/cocoa/l10n_util.mm.
  285. void Browser::ApplyForcedRTL() {
  286. NSUserDefaults* defaults = NSUserDefaults.standardUserDefaults;
  287. auto dir = base::i18n::GetForcedTextDirection();
  288. // An Electron app should respect RTL behavior of application locale over
  289. // system locale.
  290. auto should_be_rtl = dir == base::i18n::RIGHT_TO_LEFT || IsAppRTL();
  291. auto should_be_ltr = dir == base::i18n::LEFT_TO_RIGHT || !IsAppRTL();
  292. // -registerDefaults: won't do the trick here because these defaults exist
  293. // (in the global domain) to reflect the system locale. They need to be set
  294. // in Chrome's domain to supersede the system value.
  295. if (should_be_rtl) {
  296. [defaults setBool:YES forKey:@"AppleTextDirection"];
  297. [defaults setBool:YES forKey:@"NSForceRightToLeftWritingDirection"];
  298. } else if (should_be_ltr) {
  299. [defaults setBool:YES forKey:@"AppleTextDirection"];
  300. [defaults setBool:NO forKey:@"NSForceRightToLeftWritingDirection"];
  301. } else {
  302. [defaults removeObjectForKey:@"AppleTextDirection"];
  303. [defaults removeObjectForKey:@"NSForceRightToLeftWritingDirection"];
  304. }
  305. }
  306. Browser::LoginItemSettings Browser::GetLoginItemSettings(
  307. const LoginItemSettings& options) {
  308. LoginItemSettings settings;
  309. #if IS_MAS_BUILD()
  310. settings.open_at_login = platform_util::GetLoginItemEnabled();
  311. #else
  312. settings.open_at_login = CheckLoginItemStatus(&settings.open_as_hidden);
  313. settings.restore_state = base::mac::WasLaunchedAsLoginItemRestoreState();
  314. settings.opened_at_login = base::mac::WasLaunchedAsLoginOrResumeItem();
  315. settings.opened_as_hidden = base::mac::WasLaunchedAsHiddenLoginItem();
  316. #endif
  317. return settings;
  318. }
  319. void Browser::SetLoginItemSettings(LoginItemSettings settings) {
  320. #if IS_MAS_BUILD()
  321. if (!platform_util::SetLoginItemEnabled(settings.open_at_login)) {
  322. LOG(ERROR) << "Unable to set login item enabled on sandboxed app.";
  323. }
  324. #else
  325. if (settings.open_at_login) {
  326. base::mac::AddToLoginItems(base::apple::MainBundlePath(),
  327. settings.open_as_hidden);
  328. } else {
  329. base::mac::RemoveFromLoginItems(base::apple::MainBundlePath());
  330. }
  331. #endif
  332. }
  333. std::string Browser::GetExecutableFileVersion() const {
  334. return GetApplicationVersion();
  335. }
  336. std::string Browser::GetExecutableFileProductName() const {
  337. return GetApplicationName();
  338. }
  339. int Browser::DockBounce(BounceType type) {
  340. return [[AtomApplication sharedApplication]
  341. requestUserAttention:static_cast<NSRequestUserAttentionType>(type)];
  342. }
  343. void Browser::DockCancelBounce(int request_id) {
  344. [[AtomApplication sharedApplication] cancelUserAttentionRequest:request_id];
  345. }
  346. void Browser::DockSetBadgeText(const std::string& label) {
  347. NSDockTile* tile = [[AtomApplication sharedApplication] dockTile];
  348. [tile setBadgeLabel:base::SysUTF8ToNSString(label)];
  349. }
  350. void Browser::DockDownloadFinished(const std::string& filePath) {
  351. [[NSDistributedNotificationCenter defaultCenter]
  352. postNotificationName:@"com.apple.DownloadFileFinished"
  353. object:base::SysUTF8ToNSString(filePath)];
  354. }
  355. std::string Browser::DockGetBadgeText() {
  356. NSDockTile* tile = [[AtomApplication sharedApplication] dockTile];
  357. return base::SysNSStringToUTF8([tile badgeLabel]);
  358. }
  359. void Browser::DockHide() {
  360. // Transforming application state from UIElement to Foreground is an
  361. // asynchronous operation, and unfortunately there is currently no way to know
  362. // when it is finished.
  363. // So if we call DockHide => DockShow => DockHide => DockShow in a very short
  364. // time, we would trigger a bug of macOS that, there would be multiple dock
  365. // icons of the app left in system.
  366. // To work around this, we make sure DockHide does nothing if it is called
  367. // immediately after DockShow. After some experiments, 1 second seems to be
  368. // a proper interval.
  369. if (!last_dock_show_.is_null() &&
  370. base::Time::Now() - last_dock_show_ < base::Seconds(1)) {
  371. return;
  372. }
  373. for (auto* const& window : WindowList::GetWindows())
  374. [window->GetNativeWindow().GetNativeNSWindow() setCanHide:NO];
  375. ProcessSerialNumber psn = {0, kCurrentProcess};
  376. TransformProcessType(&psn, kProcessTransformToUIElementApplication);
  377. }
  378. bool Browser::DockIsVisible() {
  379. // Because DockShow has a slight delay this may not be true immediately
  380. // after that call.
  381. return ([[NSRunningApplication currentApplication] activationPolicy] ==
  382. NSApplicationActivationPolicyRegular);
  383. }
  384. v8::Local<v8::Promise> Browser::DockShow(v8::Isolate* isolate) {
  385. last_dock_show_ = base::Time::Now();
  386. gin_helper::Promise<void> promise(isolate);
  387. v8::Local<v8::Promise> handle = promise.GetHandle();
  388. BOOL active = [[NSRunningApplication currentApplication] isActive];
  389. ProcessSerialNumber psn = {0, kCurrentProcess};
  390. if (active) {
  391. // Workaround buggy behavior of TransformProcessType.
  392. // http://stackoverflow.com/questions/7596643/
  393. NSArray* runningApps = [NSRunningApplication
  394. runningApplicationsWithBundleIdentifier:@"com.apple.dock"];
  395. for (NSRunningApplication* app in runningApps) {
  396. [app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
  397. break;
  398. }
  399. __block gin_helper::Promise<void> p = std::move(promise);
  400. dispatch_time_t one_ms = dispatch_time(DISPATCH_TIME_NOW, USEC_PER_SEC);
  401. dispatch_after(one_ms, dispatch_get_main_queue(), ^{
  402. TransformProcessType(&psn, kProcessTransformToForegroundApplication);
  403. dispatch_time_t one_ms_2 = dispatch_time(DISPATCH_TIME_NOW, USEC_PER_SEC);
  404. dispatch_after(one_ms_2, dispatch_get_main_queue(), ^{
  405. [[NSRunningApplication currentApplication]
  406. activateWithOptions:NSApplicationActivateIgnoringOtherApps];
  407. p.Resolve();
  408. });
  409. });
  410. } else {
  411. TransformProcessType(&psn, kProcessTransformToForegroundApplication);
  412. promise.Resolve();
  413. }
  414. return handle;
  415. }
  416. void Browser::DockSetMenu(ElectronMenuModel* model) {
  417. ElectronApplicationDelegate* delegate =
  418. (ElectronApplicationDelegate*)[NSApp delegate];
  419. [delegate setApplicationDockMenu:model];
  420. }
  421. void Browser::DockSetIcon(v8::Isolate* isolate, v8::Local<v8::Value> icon) {
  422. gfx::Image image;
  423. if (!icon->IsNull()) {
  424. api::NativeImage* native_image = nullptr;
  425. if (!api::NativeImage::TryConvertNativeImage(isolate, icon, &native_image))
  426. return;
  427. image = native_image->image();
  428. }
  429. // This is needed to avoid a hard CHECK when this fn is called
  430. // before the browser process is ready, since supported scales
  431. // are normally set by ui::ResourceBundle::InitSharedInstance
  432. // during browser process startup.
  433. if (!is_ready())
  434. ui::SetSupportedResourceScaleFactors({ui::k100Percent});
  435. [[AtomApplication sharedApplication]
  436. setApplicationIconImage:image.AsNSImage()];
  437. }
  438. void Browser::ShowAboutPanel() {
  439. NSDictionary* options = DictionaryValueToNSDictionary(about_panel_options_);
  440. // Credits must be a NSAttributedString instead of NSString
  441. NSString* credits = (NSString*)options[@"Credits"];
  442. if (credits != nil) {
  443. NSMutableDictionary* mutable_options = [options mutableCopy];
  444. NSAttributedString* creditString = [[NSAttributedString alloc]
  445. initWithString:credits
  446. attributes:@{NSForegroundColorAttributeName : [NSColor textColor]}];
  447. [mutable_options setValue:creditString forKey:@"Credits"];
  448. options = [NSDictionary dictionaryWithDictionary:mutable_options];
  449. }
  450. [[AtomApplication sharedApplication]
  451. orderFrontStandardAboutPanelWithOptions:options];
  452. }
  453. void Browser::SetAboutPanelOptions(base::Value::Dict options) {
  454. about_panel_options_.clear();
  455. for (const auto pair : options) {
  456. std::string key = pair.first;
  457. if (!key.empty() && pair.second.is_string()) {
  458. key[0] = base::ToUpperASCII(key[0]);
  459. about_panel_options_.Set(key, pair.second.Clone());
  460. }
  461. }
  462. }
  463. void Browser::ShowEmojiPanel() {
  464. [[AtomApplication sharedApplication] orderFrontCharacterPalette:nil];
  465. }
  466. bool Browser::IsEmojiPanelSupported() {
  467. return true;
  468. }
  469. bool Browser::IsSecureKeyboardEntryEnabled() {
  470. return password_input_enabler_.get() != nullptr;
  471. }
  472. void Browser::SetSecureKeyboardEntryEnabled(bool enabled) {
  473. if (enabled) {
  474. password_input_enabler_ =
  475. std::make_unique<ui::ScopedPasswordInputEnabler>();
  476. } else {
  477. password_input_enabler_.reset();
  478. }
  479. }
  480. } // namespace electron