feat: add app.getApplicationInfoForProtocol API (#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
This commit is contained in:
parent
2cbd091e89
commit
ee61eb9aa4
6 changed files with 261 additions and 20 deletions
|
@ -815,6 +815,20 @@ Returns `String` - Name of the application handling the protocol, or an empty
|
|||
This method returns the application name of the default handler for the protocol
|
||||
(aka URI scheme) of a URL.
|
||||
|
||||
### `app.getApplicationInfoForProtocol(url)` _macOS_ _Windows_
|
||||
|
||||
* `url` String - a URL with the protocol name to check. Unlike the other
|
||||
methods in this family, this accepts an entire URL, including `://` at a
|
||||
minimum (e.g. `https://`).
|
||||
|
||||
Returns `Promise<Object>` - Resolve with an object containing the following:
|
||||
* `icon` NativeImage - the display icon of the app handling the protocol.
|
||||
* `path` String - installation path of the app handling the protocol.
|
||||
* `name` String - display name of the app handling the protocol.
|
||||
|
||||
This method returns a promise that contains the application name, icon and path of the default handler for the protocol
|
||||
(aka URI scheme) of a URL.
|
||||
|
||||
### `app.setUserTasks(tasks)` _Windows_
|
||||
|
||||
* `tasks` [Task[]](structures/task.md) - Array of `Task` objects
|
||||
|
|
|
@ -1496,6 +1496,11 @@ void App::BuildPrototype(v8::Isolate* isolate,
|
|||
.SetMethod(
|
||||
"removeAsDefaultProtocolClient",
|
||||
base::BindRepeating(&Browser::RemoveAsDefaultProtocolClient, browser))
|
||||
#if !defined(OS_LINUX)
|
||||
.SetMethod(
|
||||
"getApplicationInfoForProtocol",
|
||||
base::BindRepeating(&Browser::GetApplicationInfoForProtocol, browser))
|
||||
#endif
|
||||
.SetMethod(
|
||||
"getApplicationNameForProtocol",
|
||||
base::BindRepeating(&Browser::GetApplicationNameForProtocol, browser))
|
||||
|
|
|
@ -13,7 +13,9 @@
|
|||
#include "base/macros.h"
|
||||
#include "base/observer_list.h"
|
||||
#include "base/strings/string16.h"
|
||||
#include "base/task/cancelable_task_tracker.h"
|
||||
#include "base/values.h"
|
||||
#include "gin/dictionary.h"
|
||||
#include "shell/browser/browser_observer.h"
|
||||
#include "shell/browser/window_list_observer.h"
|
||||
#include "shell/common/gin_helper/promise.h"
|
||||
|
@ -98,6 +100,12 @@ class Browser : public WindowListObserver {
|
|||
|
||||
base::string16 GetApplicationNameForProtocol(const GURL& url);
|
||||
|
||||
#if !defined(OS_LINUX)
|
||||
// get the name, icon and path for an application
|
||||
v8::Local<v8::Promise> GetApplicationInfoForProtocol(v8::Isolate* isolate,
|
||||
const GURL& url);
|
||||
#endif
|
||||
|
||||
// Set/Get the badge count.
|
||||
bool SetBadgeCount(int count);
|
||||
int GetBadgeCount();
|
||||
|
@ -302,6 +310,9 @@ class Browser : public WindowListObserver {
|
|||
// Observers of the browser.
|
||||
base::ObserverList<BrowserObserver> observers_;
|
||||
|
||||
// Tracks tasks requesting file icons.
|
||||
base::CancelableTaskTracker cancelable_task_tracker_;
|
||||
|
||||
// Whether `app.exit()` has been called
|
||||
bool is_exiting_ = false;
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include "shell/browser/native_window.h"
|
||||
#include "shell/browser/window_list.h"
|
||||
#include "shell/common/application_info.h"
|
||||
#include "shell/common/gin_converters/image_converter.h"
|
||||
#include "shell/common/gin_helper/arguments.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/gin_helper/error_thrower.h"
|
||||
|
@ -31,6 +32,65 @@
|
|||
|
||||
namespace electron {
|
||||
|
||||
namespace {
|
||||
|
||||
NSString* GetAppPathForProtocol(const GURL& url) {
|
||||
NSURL* ns_url = [NSURL
|
||||
URLWithString:base::SysUTF8ToNSString(url.possibly_invalid_spec())];
|
||||
base::ScopedCFTypeRef<CFErrorRef> out_err;
|
||||
|
||||
base::ScopedCFTypeRef<CFURLRef> openingApp(LSCopyDefaultApplicationURLForURL(
|
||||
(CFURLRef)ns_url, kLSRolesAll, out_err.InitializeInto()));
|
||||
|
||||
if (out_err) {
|
||||
// likely kLSApplicationNotFoundErr
|
||||
return nullptr;
|
||||
}
|
||||
NSString* app_path = [base::mac::CFToNSCast(openingApp.get()) path];
|
||||
return app_path;
|
||||
}
|
||||
|
||||
gfx::Image GetApplicationIconForProtocol(NSString* _Nonnull app_path) {
|
||||
NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile:app_path];
|
||||
gfx::Image icon(image);
|
||||
return icon;
|
||||
}
|
||||
|
||||
base::string16 GetAppDisplayNameForProtocol(NSString* app_path) {
|
||||
NSString* app_display_name =
|
||||
[[NSFileManager defaultManager] displayNameAtPath:app_path];
|
||||
return base::SysNSStringToUTF16(app_display_name);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
v8::Local<v8::Promise> Browser::GetApplicationInfoForProtocol(
|
||||
v8::Isolate* isolate,
|
||||
const GURL& url) {
|
||||
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
|
||||
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||
gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(isolate);
|
||||
|
||||
NSString* ns_app_path = GetAppPathForProtocol(url);
|
||||
|
||||
if (!ns_app_path) {
|
||||
promise.RejectWithErrorMessage(
|
||||
"Unable to retrieve installation path to app");
|
||||
return handle;
|
||||
}
|
||||
|
||||
base::string16 app_path = base::SysNSStringToUTF16(ns_app_path);
|
||||
base::string16 app_display_name = GetAppDisplayNameForProtocol(ns_app_path);
|
||||
gfx::Image app_icon = GetApplicationIconForProtocol(ns_app_path);
|
||||
|
||||
dict.Set("name", app_display_name);
|
||||
dict.Set("path", app_path);
|
||||
dict.Set("icon", app_icon);
|
||||
|
||||
promise.Resolve(dict);
|
||||
return handle;
|
||||
}
|
||||
|
||||
void Browser::SetShutdownHandler(base::Callback<bool()> handler) {
|
||||
[[AtomApplication sharedApplication] setShutdownHandler:std::move(handler)];
|
||||
}
|
||||
|
@ -148,19 +208,12 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol,
|
|||
}
|
||||
|
||||
base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
|
||||
NSURL* ns_url = [NSURL
|
||||
URLWithString:base::SysUTF8ToNSString(url.possibly_invalid_spec())];
|
||||
base::ScopedCFTypeRef<CFErrorRef> out_err;
|
||||
base::ScopedCFTypeRef<CFURLRef> openingApp(LSCopyDefaultApplicationURLForURL(
|
||||
(CFURLRef)ns_url, kLSRolesAll, out_err.InitializeInto()));
|
||||
if (out_err) {
|
||||
// likely kLSApplicationNotFoundErr
|
||||
NSString* app_path = GetAppPathForProtocol(url);
|
||||
if (!app_path) {
|
||||
return base::string16();
|
||||
}
|
||||
NSString* appPath = [base::mac::CFToNSCast(openingApp.get()) path];
|
||||
NSString* appDisplayName =
|
||||
[[NSFileManager defaultManager] displayNameAtPath:appPath];
|
||||
return base::SysNSStringToUTF16(appDisplayName);
|
||||
base::string16 app_display_name = GetAppDisplayNameForProtocol(app_path);
|
||||
return app_display_name;
|
||||
}
|
||||
|
||||
void Browser::SetAppUserModelID(const base::string16& name) {}
|
||||
|
|
|
@ -21,11 +21,17 @@
|
|||
#include "base/win/registry.h"
|
||||
#include "base/win/win_util.h"
|
||||
#include "base/win/windows_version.h"
|
||||
#include "chrome/browser/icon_manager.h"
|
||||
#include "electron/electron_version.h"
|
||||
#include "shell/browser/api/electron_api_app.h"
|
||||
#include "shell/browser/electron_browser_main_parts.h"
|
||||
#include "shell/browser/ui/message_box.h"
|
||||
#include "shell/browser/ui/win/jump_list.h"
|
||||
#include "shell/common/application_info.h"
|
||||
#include "shell/common/gin_converters/file_path_converter.h"
|
||||
#include "shell/common/gin_converters/image_converter.h"
|
||||
#include "shell/common/gin_helper/arguments.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/skia_util.h"
|
||||
#include "ui/events/keycodes/keyboard_code_conversion_win.h"
|
||||
|
||||
|
@ -49,7 +55,6 @@ BOOL CALLBACK WindowsEnumerationHandler(HWND hwnd, LPARAM param) {
|
|||
bool GetProcessExecPath(base::string16* exe) {
|
||||
base::FilePath path;
|
||||
if (!base::PathService::Get(base::FILE_EXE, &path)) {
|
||||
LOG(ERROR) << "Error getting app exe path";
|
||||
return false;
|
||||
}
|
||||
*exe = path.value();
|
||||
|
@ -81,23 +86,25 @@ bool IsValidCustomProtocol(const base::string16& scheme) {
|
|||
return cmd_key.Valid() && cmd_key.HasValue(L"URL Protocol");
|
||||
}
|
||||
|
||||
// Helper for GetApplicationInfoForProtocol().
|
||||
// takes in an assoc_str
|
||||
// (https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/ne-shlwapi-assocstr)
|
||||
// and returns the application name, icon and path that handles the protocol.
|
||||
//
|
||||
// Windows 8 introduced a new protocol->executable binding system which cannot
|
||||
// be retrieved in the HKCR registry subkey method implemented below. We call
|
||||
// AssocQueryString with the new Win8-only flag ASSOCF_IS_PROTOCOL instead.
|
||||
base::string16 GetAppForProtocolUsingAssocQuery(const GURL& url) {
|
||||
base::string16 GetAppInfoHelperForProtocol(ASSOCSTR assoc_str,
|
||||
const GURL& url) {
|
||||
const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme());
|
||||
if (!IsValidCustomProtocol(url_scheme))
|
||||
return base::string16();
|
||||
|
||||
// Query AssocQueryString for a human-readable description of the program
|
||||
// that will be invoked given the provided URL spec. This is used only to
|
||||
// populate the external protocol dialog box the user sees when invoking
|
||||
// an unknown external protocol.
|
||||
wchar_t out_buffer[1024];
|
||||
DWORD buffer_size = base::size(out_buffer);
|
||||
HRESULT hr =
|
||||
AssocQueryString(ASSOCF_IS_PROTOCOL, ASSOCSTR_FRIENDLYAPPNAME,
|
||||
url_scheme.c_str(), NULL, out_buffer, &buffer_size);
|
||||
AssocQueryString(ASSOCF_IS_PROTOCOL, assoc_str, url_scheme.c_str(), NULL,
|
||||
out_buffer, &buffer_size);
|
||||
if (FAILED(hr)) {
|
||||
DLOG(WARNING) << "AssocQueryString failed!";
|
||||
return base::string16();
|
||||
|
@ -105,6 +112,32 @@ base::string16 GetAppForProtocolUsingAssocQuery(const GURL& url) {
|
|||
return base::string16(out_buffer);
|
||||
}
|
||||
|
||||
void OnIconDataAvailable(const base::FilePath& app_path,
|
||||
const base::string16& app_display_name,
|
||||
gin_helper::Promise<gin_helper::Dictionary> promise,
|
||||
gfx::Image icon) {
|
||||
if (!icon.IsEmpty()) {
|
||||
v8::HandleScope scope(promise.isolate());
|
||||
gin_helper::Dictionary dict =
|
||||
gin::Dictionary::CreateEmpty(promise.isolate());
|
||||
|
||||
dict.Set("path", app_path);
|
||||
dict.Set("name", app_display_name);
|
||||
dict.Set("icon", icon);
|
||||
promise.Resolve(dict);
|
||||
} else {
|
||||
promise.RejectWithErrorMessage("Failed to get file icon.");
|
||||
}
|
||||
}
|
||||
|
||||
base::string16 GetAppDisplayNameForProtocol(const GURL& url) {
|
||||
return GetAppInfoHelperForProtocol(ASSOCSTR_FRIENDLYAPPNAME, url);
|
||||
}
|
||||
|
||||
base::string16 GetAppPathForProtocol(const GURL& url) {
|
||||
return GetAppInfoHelperForProtocol(ASSOCSTR_EXECUTABLE, url);
|
||||
}
|
||||
|
||||
base::string16 GetAppForProtocolUsingRegistry(const GURL& url) {
|
||||
const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme());
|
||||
if (!IsValidCustomProtocol(url_scheme))
|
||||
|
@ -169,6 +202,96 @@ void Browser::Focus(gin_helper::Arguments* args) {
|
|||
EnumWindows(&WindowsEnumerationHandler, reinterpret_cast<LPARAM>(&pid));
|
||||
}
|
||||
|
||||
void GetFileIcon(const base::FilePath& path,
|
||||
v8::Isolate* isolate,
|
||||
base::CancelableTaskTracker* cancelable_task_tracker_,
|
||||
const base::string16 app_display_name,
|
||||
gin_helper::Promise<gin_helper::Dictionary> promise) {
|
||||
base::FilePath normalized_path = path.NormalizePathSeparators();
|
||||
IconLoader::IconSize icon_size = IconLoader::IconSize::LARGE;
|
||||
|
||||
auto* icon_manager = ElectronBrowserMainParts::Get()->GetIconManager();
|
||||
gfx::Image* icon =
|
||||
icon_manager->LookupIconFromFilepath(normalized_path, icon_size);
|
||||
if (icon) {
|
||||
gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(isolate);
|
||||
dict.Set("icon", *icon);
|
||||
dict.Set("name", app_display_name);
|
||||
dict.Set("path", normalized_path);
|
||||
promise.Resolve(dict);
|
||||
} else {
|
||||
icon_manager->LoadIcon(normalized_path, icon_size,
|
||||
base::BindOnce(&OnIconDataAvailable, normalized_path,
|
||||
app_display_name, std::move(promise)),
|
||||
cancelable_task_tracker_);
|
||||
}
|
||||
}
|
||||
|
||||
void GetApplicationInfoForProtocolUsingRegistry(
|
||||
v8::Isolate* isolate,
|
||||
const GURL& url,
|
||||
gin_helper::Promise<gin_helper::Dictionary> promise,
|
||||
base::CancelableTaskTracker* cancelable_task_tracker_) {
|
||||
base::FilePath app_path;
|
||||
|
||||
const base::string16 url_scheme = base::ASCIIToUTF16(url.scheme());
|
||||
if (!IsValidCustomProtocol(url_scheme)) {
|
||||
promise.RejectWithErrorMessage("invalid url_scheme");
|
||||
return;
|
||||
}
|
||||
base::string16 command_to_launch;
|
||||
const base::string16 cmd_key_path = url_scheme + L"\\shell\\open\\command";
|
||||
base::win::RegKey cmd_key_exe(HKEY_CLASSES_ROOT, cmd_key_path.c_str(),
|
||||
KEY_READ);
|
||||
if (cmd_key_exe.ReadValue(NULL, &command_to_launch) == ERROR_SUCCESS) {
|
||||
base::CommandLine command_line(
|
||||
base::CommandLine::FromString(command_to_launch));
|
||||
app_path = command_line.GetProgram();
|
||||
} else {
|
||||
promise.RejectWithErrorMessage(
|
||||
"Unable to retrieve installation path to app");
|
||||
return;
|
||||
}
|
||||
const base::string16 app_display_name = GetAppForProtocolUsingRegistry(url);
|
||||
|
||||
if (app_display_name.length() == 0) {
|
||||
promise.RejectWithErrorMessage(
|
||||
"Unable to retrieve application display name");
|
||||
return;
|
||||
}
|
||||
GetFileIcon(app_path, isolate, cancelable_task_tracker_, app_display_name,
|
||||
std::move(promise));
|
||||
}
|
||||
|
||||
// resolves `Promise<Object>` - Resolve with an object containing the following:
|
||||
// * `icon` NativeImage - the display icon of the app handling the protocol.
|
||||
// * `path` String - installation path of the app handling the protocol.
|
||||
// * `name` String - display name of the app handling the protocol.
|
||||
void GetApplicationInfoForProtocolUsingAssocQuery(
|
||||
v8::Isolate* isolate,
|
||||
const GURL& url,
|
||||
gin_helper::Promise<gin_helper::Dictionary> promise,
|
||||
base::CancelableTaskTracker* cancelable_task_tracker_) {
|
||||
base::string16 app_path = GetAppPathForProtocol(url);
|
||||
|
||||
if (app_path.empty()) {
|
||||
promise.RejectWithErrorMessage(
|
||||
"Unable to retrieve installation path to app");
|
||||
return;
|
||||
}
|
||||
|
||||
base::string16 app_display_name = GetAppDisplayNameForProtocol(url);
|
||||
|
||||
if (app_display_name.empty()) {
|
||||
promise.RejectWithErrorMessage("Unable to retrieve display name of app");
|
||||
return;
|
||||
}
|
||||
|
||||
base::FilePath app_path_file_path = base::FilePath(app_path);
|
||||
GetFileIcon(app_path_file_path, isolate, cancelable_task_tracker_,
|
||||
app_display_name, std::move(promise));
|
||||
}
|
||||
|
||||
void Browser::AddRecentDocument(const base::FilePath& path) {
|
||||
CComPtr<IShellItem> item;
|
||||
HRESULT hr = SHCreateItemFromParsingName(path.value().c_str(), NULL,
|
||||
|
@ -358,7 +481,7 @@ bool Browser::IsDefaultProtocolClient(const std::string& protocol,
|
|||
base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
|
||||
// Windows 8 or above has a new protocol association query.
|
||||
if (base::win::GetVersion() >= base::win::Version::WIN8) {
|
||||
base::string16 application_name = GetAppForProtocolUsingAssocQuery(url);
|
||||
base::string16 application_name = GetAppDisplayNameForProtocol(url);
|
||||
if (!application_name.empty())
|
||||
return application_name;
|
||||
}
|
||||
|
@ -366,6 +489,24 @@ base::string16 Browser::GetApplicationNameForProtocol(const GURL& url) {
|
|||
return GetAppForProtocolUsingRegistry(url);
|
||||
}
|
||||
|
||||
v8::Local<v8::Promise> Browser::GetApplicationInfoForProtocol(
|
||||
v8::Isolate* isolate,
|
||||
const GURL& url) {
|
||||
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
|
||||
v8::Local<v8::Promise> handle = promise.GetHandle();
|
||||
|
||||
// Windows 8 or above has a new protocol association query.
|
||||
if (base::win::GetVersion() >= base::win::Version::WIN8) {
|
||||
GetApplicationInfoForProtocolUsingAssocQuery(
|
||||
isolate, url, std::move(promise), &cancelable_task_tracker_);
|
||||
return handle;
|
||||
}
|
||||
|
||||
GetApplicationInfoForProtocolUsingRegistry(isolate, url, std::move(promise),
|
||||
&cancelable_task_tracker_);
|
||||
return handle;
|
||||
}
|
||||
|
||||
bool Browser::SetBadgeCount(int count) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -973,6 +973,23 @@ describe('app module', () => {
|
|||
});
|
||||
});
|
||||
|
||||
ifdescribe(process.platform !== 'linux')('getApplicationInfoForProtocol()', () => {
|
||||
it('returns promise rejection for a bogus protocol', async function () {
|
||||
await expect(
|
||||
app.getApplicationInfoForProtocol('bogus-protocol://')
|
||||
).to.eventually.be.rejectedWith(
|
||||
'Unable to retrieve installation path to app'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns resolved promise with appPath, displayName and icon', async function () {
|
||||
const appInfo = await app.getApplicationInfoForProtocol('https://');
|
||||
expect(appInfo.path).not.to.be.undefined();
|
||||
expect(appInfo.name).not.to.be.undefined();
|
||||
expect(appInfo.icon).not.to.be.undefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDefaultProtocolClient()', () => {
|
||||
it('returns false for a bogus protocol', () => {
|
||||
expect(app.isDefaultProtocolClient('bogus-protocol://')).to.equal(false);
|
||||
|
|
Loading…
Reference in a new issue