Skip to content

Commit ee61eb9

Browse files
authored
feat: add app.getApplicationInfoForProtocol API (electron#24112)
* pre merge * windows changes * added tests * clean up * more cleanup * lint error * windows 7 support * added windows 7 implementation * code review * lint and code review * code review * app.md merge conflict * merge conflict app.md accidently deleted code block * 'lint' * mis-moved getapplicationinfoforprotocol() into anonymous namespace * fix test * lint * code review
1 parent 2cbd091 commit ee61eb9

File tree

6 files changed

+261
-20
lines changed

6 files changed

+261
-20
lines changed

docs/api/app.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,20 @@ Returns `String` - Name of the application handling the protocol, or an empty
815815
This method returns the application name of the default handler for the protocol
816816
(aka URI scheme) of a URL.
817817

818+
### `app.getApplicationInfoForProtocol(url)` _macOS_ _Windows_
819+
820+
* `url` String - a URL with the protocol name to check. Unlike the other
821+
methods in this family, this accepts an entire URL, including `://` at a
822+
minimum (e.g. `https://`).
823+
824+
Returns `Promise<Object>` - Resolve with an object containing the following:
825+
* `icon` NativeImage - the display icon of the app handling the protocol.
826+
* `path` String - installation path of the app handling the protocol.
827+
* `name` String - display name of the app handling the protocol.
828+
829+
This method returns a promise that contains the application name, icon and path of the default handler for the protocol
830+
(aka URI scheme) of a URL.
831+
818832
### `app.setUserTasks(tasks)` _Windows_
819833

820834
* `tasks` [Task[]](structures/task.md) - Array of `Task` objects

shell/browser/api/electron_api_app.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,6 +1496,11 @@ void App::BuildPrototype(v8::Isolate* isolate,
14961496
.SetMethod(
14971497
"removeAsDefaultProtocolClient",
14981498
base::BindRepeating(&Browser::RemoveAsDefaultProtocolClient, browser))
1499+
#if !defined(OS_LINUX)
1500+
.SetMethod(
1501+
"getApplicationInfoForProtocol",
1502+
base::BindRepeating(&Browser::GetApplicationInfoForProtocol, browser))
1503+
#endif
14991504
.SetMethod(
15001505
"getApplicationNameForProtocol",
15011506
base::BindRepeating(&Browser::GetApplicationNameForProtocol, browser))

shell/browser/browser.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
#include "base/macros.h"
1414
#include "base/observer_list.h"
1515
#include "base/strings/string16.h"
16+
#include "base/task/cancelable_task_tracker.h"
1617
#include "base/values.h"
18+
#include "gin/dictionary.h"
1719
#include "shell/browser/browser_observer.h"
1820
#include "shell/browser/window_list_observer.h"
1921
#include "shell/common/gin_helper/promise.h"
@@ -98,6 +100,12 @@ class Browser : public WindowListObserver {
98100

99101
base::string16 GetApplicationNameForProtocol(const GURL& url);
100102

103+
#if !defined(OS_LINUX)
104+
// get the name, icon and path for an application
105+
v8::Local<v8::Promise> GetApplicationInfoForProtocol(v8::Isolate* isolate,
106+
const GURL& url);
107+
#endif
108+
101109
// Set/Get the badge count.
102110
bool SetBadgeCount(int count);
103111
int GetBadgeCount();
@@ -302,6 +310,9 @@ class Browser : public WindowListObserver {
302310
// Observers of the browser.
303311
base::ObserverList<BrowserObserver> observers_;
304312

313+
// Tracks tasks requesting file icons.
314+
base::CancelableTaskTracker cancelable_task_tracker_;
315+
305316
// Whether `app.exit()` has been called
306317
bool is_exiting_ = false;
307318

shell/browser/browser_mac.mm

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "shell/browser/native_window.h"
2222
#include "shell/browser/window_list.h"
2323
#include "shell/common/application_info.h"
24+
#include "shell/common/gin_converters/image_converter.h"
2425
#include "shell/common/gin_helper/arguments.h"
2526
#include "shell/common/gin_helper/dictionary.h"
2627
#include "shell/common/gin_helper/error_thrower.h"
@@ -31,6 +32,65 @@
3132

3233
namespace electron {
3334

35+
namespace {
36+
37+
NSString* GetAppPathForProtocol(const GURL& url) {
38+
NSURL* ns_url = [NSURL
39+
URLWithString:base::SysUTF8ToNSString(url.possibly_invalid_spec())];
40+
base::ScopedCFTypeRef<CFErrorRef> out_err;
41+
42+
base::ScopedCFTypeRef<CFURLRef> openingApp(LSCopyDefaultApplicationURLForURL(
43+
(CFURLRef)ns_url, kLSRolesAll, out_err.InitializeInto()));
44+
45+
if (out_err) {
46+
// likely kLSApplicationNotFoundErr
47+
return nullptr;
48+
}
49+
NSString* app_path = [base::mac::CFToNSCast(openingApp.get()) path];
50+
return app_path;
51+
}
52+
53+
gfx::Image GetApplicationIconForProtocol(NSString* _Nonnull app_path) {
54+
NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile:app_path];
55+
gfx::Image icon(image);
56+
return icon;
57+
}
58+
59+
base::string16 GetAppDisplayNameForProtocol(NSString* app_path) {
60+
NSString* app_display_name =
61+
[[NSFileManager defaultManager] displayNameAtPath:app_path];
62+
return base::SysNSStringToUTF16(app_display_name);
63+
}
64+
65+
} // namespace
66+
67+
v8::Local<v8::Promise> Browser::GetApplicationInfoForProtocol(
68+
v8::Isolate* isolate,
69+
const GURL& url) {
70+
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
71+
v8::Local<v8::Promise> handle = promise.GetHandle();
72+
gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(isolate);
73+
74+
NSString* ns_app_path = GetAppPathForProtocol(url);
75+
76+
if (!ns_app_path) {
77+
promise.RejectWithErrorMessage(
78+
"Unable to retrieve installation path to app");
79+
return handle;
80+
}
81+
82+
base::string16 app_path = base::SysNSStringToUTF16(ns_app_path);
83+
base::string16 app_display_name = GetAppDisplayNameForProtocol(ns_app_path);
84+
gfx::Image app_icon = GetApplicationIconForProtocol(ns_app_path);
85+
86+
dict.Set("name", app_display_name);
87+
dict.Set("path", app_path);
88+
dict.Set("icon", app_icon);
89+
90+
promise.Resolve(dict);
91+
return handle;
92+
}
93+
3494
void Browser::SetShutdownHandler(base::Callback<bool()> handler) {
3595
[[AtomApplication sharedApplication] setShutdownHandler:std::move(handler)];
3696
}
@@ -148,19 +208,12 @@
148208
}
149209

150210
base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
151-
NSURL* ns_url = [NSURL
152-
URLWithString:base::SysUTF8ToNSString(url.possibly_invalid_spec())];
153-
base::ScopedCFTypeRef<CFErrorRef> out_err;
154-
base::ScopedCFTypeRef<CFURLRef> openingApp(LSCopyDefaultApplicationURLForURL(
155-
(CFURLRef)ns_url, kLSRolesAll, out_err.InitializeInto()));
156-
if (out_err) {
157-
// likely kLSApplicationNotFoundErr
211+
NSString* app_path = GetAppPathForProtocol(url);
212+
if (!app_path) {
158213
return base::string16();
159214
}
160-
NSString* appPath = [base::mac::CFToNSCast(openingApp.get()) path];
161-
NSString* appDisplayName =
162-
[[NSFileManager defaultManager] displayNameAtPath:appPath];
163-
return base::SysNSStringToUTF16(appDisplayName);
215+
base::string16 app_display_name = GetAppDisplayNameForProtocol(app_path);
216+
return app_display_name;
164217
}
165218

166219
void Browser::SetAppUserModelID(const base::string16& name) {}

shell/browser/browser_win.cc

Lines changed: 150 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@
2121
#include "base/win/registry.h"
2222
#include "base/win/win_util.h"
2323
#include "base/win/windows_version.h"
24+
#include "chrome/browser/icon_manager.h"
2425
#include "electron/electron_version.h"
26+
#include "shell/browser/api/electron_api_app.h"
27+
#include "shell/browser/electron_browser_main_parts.h"
2528
#include "shell/browser/ui/message_box.h"
2629
#include "shell/browser/ui/win/jump_list.h"
2730
#include "shell/common/application_info.h"
31+
#include "shell/common/gin_converters/file_path_converter.h"
32+
#include "shell/common/gin_converters/image_converter.h"
2833
#include "shell/common/gin_helper/arguments.h"
34+
#include "shell/common/gin_helper/dictionary.h"
2935
#include "shell/common/skia_util.h"
3036
#include "ui/events/keycodes/keyboard_code_conversion_win.h"
3137

@@ -49,7 +55,6 @@ BOOL CALLBACK WindowsEnumerationHandler(HWND hwnd, LPARAM param) {
4955
bool GetProcessExecPath(base::string16* exe) {
5056
base::FilePath path;
5157
if (!base::PathService::Get(base::FILE_EXE, &path)) {
52-
LOG(ERROR) << "Error getting app exe path";
5358
return false;
5459
}
5560
*exe = path.value();
@@ -81,30 +86,58 @@ bool IsValidCustomProtocol(const base::string16& scheme) {
8186
return cmd_key.Valid() && cmd_key.HasValue(L"URL Protocol");
8287
}
8388

89+
// Helper for GetApplicationInfoForProtocol().
90+
// takes in an assoc_str
91+
// (https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/ne-shlwapi-assocstr)
92+
// and returns the application name, icon and path that handles the protocol.
93+
//
8494
// Windows 8 introduced a new protocol->executable binding system which cannot
8595
// be retrieved in the HKCR registry subkey method implemented below. We call
8696
// AssocQueryString with the new Win8-only flag ASSOCF_IS_PROTOCOL instead.
87-
base::string16 GetAppForProtocolUsingAssocQuery(const GURL& url) {
97+
base::string16 GetAppInfoHelperForProtocol(ASSOCSTR assoc_str,
98+
const GURL& url) {
8899
const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme());
89100
if (!IsValidCustomProtocol(url_scheme))
90101
return base::string16();
91102

92-
// Query AssocQueryString for a human-readable description of the program
93-
// that will be invoked given the provided URL spec. This is used only to
94-
// populate the external protocol dialog box the user sees when invoking
95-
// an unknown external protocol.
96103
wchar_t out_buffer[1024];
97104
DWORD buffer_size = base::size(out_buffer);
98105
HRESULT hr =
99-
AssocQueryString(ASSOCF_IS_PROTOCOL, ASSOCSTR_FRIENDLYAPPNAME,
100-
url_scheme.c_str(), NULL, out_buffer, &buffer_size);
106+
AssocQueryString(ASSOCF_IS_PROTOCOL, assoc_str, url_scheme.c_str(), NULL,
107+
out_buffer, &buffer_size);
101108
if (FAILED(hr)) {
102109
DLOG(WARNING) << "AssocQueryString failed!";
103110
return base::string16();
104111
}
105112
return base::string16(out_buffer);
106113
}
107114

115+
void OnIconDataAvailable(const base::FilePath& app_path,
116+
const base::string16& app_display_name,
117+
gin_helper::Promise<gin_helper::Dictionary> promise,
118+
gfx::Image icon) {
119+
if (!icon.IsEmpty()) {
120+
v8::HandleScope scope(promise.isolate());
121+
gin_helper::Dictionary dict =
122+
gin::Dictionary::CreateEmpty(promise.isolate());
123+
124+
dict.Set("path", app_path);
125+
dict.Set("name", app_display_name);
126+
dict.Set("icon", icon);
127+
promise.Resolve(dict);
128+
} else {
129+
promise.RejectWithErrorMessage("Failed to get file icon.");
130+
}
131+
}
132+
133+
base::string16 GetAppDisplayNameForProtocol(const GURL& url) {
134+
return GetAppInfoHelperForProtocol(ASSOCSTR_FRIENDLYAPPNAME, url);
135+
}
136+
137+
base::string16 GetAppPathForProtocol(const GURL& url) {
138+
return GetAppInfoHelperForProtocol(ASSOCSTR_EXECUTABLE, url);
139+
}
140+
108141
base::string16 GetAppForProtocolUsingRegistry(const GURL& url) {
109142
const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme());
110143
if (!IsValidCustomProtocol(url_scheme))
@@ -169,6 +202,96 @@ void Browser::Focus(gin_helper::Arguments* args) {
169202
EnumWindows(&WindowsEnumerationHandler, reinterpret_cast<LPARAM>(&pid));
170203
}
171204

205+
void GetFileIcon(const base::FilePath& path,
206+
v8::Isolate* isolate,
207+
base::CancelableTaskTracker* cancelable_task_tracker_,
208+
const base::string16 app_display_name,
209+
gin_helper::Promise<gin_helper::Dictionary> promise) {
210+
base::FilePath normalized_path = path.NormalizePathSeparators();
211+
IconLoader::IconSize icon_size = IconLoader::IconSize::LARGE;
212+
213+
auto* icon_manager = ElectronBrowserMainParts::Get()->GetIconManager();
214+
gfx::Image* icon =
215+
icon_manager->LookupIconFromFilepath(normalized_path, icon_size);
216+
if (icon) {
217+
gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(isolate);
218+
dict.Set("icon", *icon);
219+
dict.Set("name", app_display_name);
220+
dict.Set("path", normalized_path);
221+
promise.Resolve(dict);
222+
} else {
223+
icon_manager->LoadIcon(normalized_path, icon_size,
224+
base::BindOnce(&OnIconDataAvailable, normalized_path,
225+
app_display_name, std::move(promise)),
226+
cancelable_task_tracker_);
227+
}
228+
}
229+
230+
void GetApplicationInfoForProtocolUsingRegistry(
231+
v8::Isolate* isolate,
232+
const GURL& url,
233+
gin_helper::Promise<gin_helper::Dictionary> promise,
234+
base::CancelableTaskTracker* cancelable_task_tracker_) {
235+
base::FilePath app_path;
236+
237+
const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme());
238+
if (!IsValidCustomProtocol(url_scheme)) {
239+
promise.RejectWithErrorMessage("invalid url_scheme");
240+
return;
241+
}
242+
base::string16 command_to_launch;
243+
const base::string16 cmd_key_path = url_scheme + L"\\shell\\open\\command";
244+
base::win::RegKey cmd_key_exe(HKEY_CLASSES_ROOT, cmd_key_path.c_str(),
245+
KEY_READ);
246+
if (cmd_key_exe.ReadValue(NULL, &command_to_launch) == ERROR_SUCCESS) {
247+
base::CommandLine command_line(
248+
base::CommandLine::FromString(command_to_launch));
249+
app_path = command_line.GetProgram();
250+
} else {
251+
promise.RejectWithErrorMessage(
252+
"Unable to retrieve installation path to app");
253+
return;
254+
}
255+
const base::string16 app_display_name = GetAppForProtocolUsingRegistry(url);
256+
257+
if (app_display_name.length() == 0) {
258+
promise.RejectWithErrorMessage(
259+
"Unable to retrieve application display name");
260+
return;
261+
}
262+
GetFileIcon(app_path, isolate, cancelable_task_tracker_, app_display_name,
263+
std::move(promise));
264+
}
265+
266+
// resolves `Promise<Object>` - Resolve with an object containing the following:
267+
// * `icon` NativeImage - the display icon of the app handling the protocol.
268+
// * `path` String - installation path of the app handling the protocol.
269+
// * `name` String - display name of the app handling the protocol.
270+
void GetApplicationInfoForProtocolUsingAssocQuery(
271+
v8::Isolate* isolate,
272+
const GURL& url,
273+
gin_helper::Promise<gin_helper::Dictionary> promise,
274+
base::CancelableTaskTracker* cancelable_task_tracker_) {
275+
base::string16 app_path = GetAppPathForProtocol(url);
276+
277+
if (app_path.empty()) {
278+
promise.RejectWithErrorMessage(
279+
"Unable to retrieve installation path to app");
280+
return;
281+
}
282+
283+
base::string16 app_display_name = GetAppDisplayNameForProtocol(url);
284+
285+
if (app_display_name.empty()) {
286+
promise.RejectWithErrorMessage("Unable to retrieve display name of app");
287+
return;
288+
}
289+
290+
base::FilePath app_path_file_path = base::FilePath(app_path);
291+
GetFileIcon(app_path_file_path, isolate, cancelable_task_tracker_,
292+
app_display_name, std::move(promise));
293+
}
294+
172295
void Browser::AddRecentDocument(const base::FilePath& path) {
173296
CComPtr<IShellItem> item;
174297
HRESULT hr = SHCreateItemFromParsingName(path.value().c_str(), NULL,
@@ -358,14 +481,32 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol,
358481
base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
359482
// Windows 8 or above has a new protocol association query.
360483
if (base::win::GetVersion() >= base::win::Version::WIN8) {
361-
base::string16 application_name = GetAppForProtocolUsingAssocQuery(url);
484+
base::string16 application_name = GetAppDisplayNameForProtocol(url);
362485
if (!application_name.empty())
363486
return application_name;
364487
}
365488

366489
return GetAppForProtocolUsingRegistry(url);
367490
}
368491

492+
v8::Local<v8::Promise> Browser::GetApplicationInfoForProtocol(
493+
v8::Isolate* isolate,
494+
const GURL& url) {
495+
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
496+
v8::Local<v8::Promise> handle = promise.GetHandle();
497+
498+
// Windows 8 or above has a new protocol association query.
499+
if (base::win::GetVersion() >= base::win::Version::WIN8) {
500+
GetApplicationInfoForProtocolUsingAssocQuery(
501+
isolate, url, std::move(promise), &cancelable_task_tracker_);
502+
return handle;
503+
}
504+
505+
GetApplicationInfoForProtocolUsingRegistry(isolate, url, std::move(promise),
506+
&cancelable_task_tracker_);
507+
return handle;
508+
}
509+
369510
bool Browser::SetBadgeCount(int count) {
370511
return false;
371512
}

0 commit comments

Comments
 (0)