Skip to content

Commit ef88f36

Browse files
committed
feat: add cross-platform UrlOpener API and C bindings
1 parent 55f15ef commit ef88f36

15 files changed

+592
-0
lines changed

CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.10)
22

33
project(nativeapi_library VERSION 0.0.1 LANGUAGES CXX C)
44

5+
include(CTest)
6+
57
# Add library subdirectory
68
add_subdirectory(src)
79

@@ -24,3 +26,7 @@ add_subdirectory(examples/tray_icon_example)
2426
add_subdirectory(examples/tray_icon_c_example)
2527
add_subdirectory(examples/window_c_example)
2628
add_subdirectory(examples/window_example)
29+
30+
if(BUILD_TESTING)
31+
add_subdirectory(tests)
32+
endif()

include/nativeapi.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "../src/tray_icon.h"
3030
#include "../src/tray_icon_event.h"
3131
#include "../src/tray_manager.h"
32+
#include "../src/url_opener.h"
3233
#include "../src/window.h"
3334
#include "../src/window_event.h"
3435
#include "../src/window_manager.h"
@@ -52,5 +53,6 @@
5253
#include "../src/capi/string_utils_c.h"
5354
#include "../src/capi/tray_icon_c.h"
5455
#include "../src/capi/tray_manager_c.h"
56+
#include "../src/capi/url_opener_c.h"
5557
#include "../src/capi/window_c.h"
5658
#include "../src/capi/window_manager_c.h"

src/capi/url_opener_c.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#include "url_opener_c.h"
2+
3+
#include <exception>
4+
#include <string>
5+
6+
#include "../url_opener.h"
7+
#include "string_utils_c.h"
8+
9+
using namespace nativeapi;
10+
11+
namespace {
12+
13+
native_url_open_error_code_t ToCErrorCode(UrlOpenErrorCode code) {
14+
switch (code) {
15+
case UrlOpenErrorCode::kNone:
16+
return NATIVE_URL_OPEN_ERROR_NONE;
17+
case UrlOpenErrorCode::kInvalidUrlEmpty:
18+
return NATIVE_URL_OPEN_ERROR_INVALID_URL_EMPTY;
19+
case UrlOpenErrorCode::kInvalidUrlMissingScheme:
20+
return NATIVE_URL_OPEN_ERROR_INVALID_URL_MISSING_SCHEME;
21+
case UrlOpenErrorCode::kInvalidUrlUnsupportedScheme:
22+
return NATIVE_URL_OPEN_ERROR_INVALID_URL_UNSUPPORTED_SCHEME;
23+
case UrlOpenErrorCode::kUnsupportedPlatform:
24+
return NATIVE_URL_OPEN_ERROR_UNSUPPORTED_PLATFORM;
25+
case UrlOpenErrorCode::kInvocationFailed:
26+
return NATIVE_URL_OPEN_ERROR_INVOCATION_FAILED;
27+
default:
28+
return NATIVE_URL_OPEN_ERROR_INVOCATION_FAILED;
29+
}
30+
}
31+
32+
native_url_open_result_t MakeResult(bool success,
33+
native_url_open_error_code_t error_code,
34+
const std::string& message) {
35+
native_url_open_result_t result = {};
36+
result.success = success;
37+
result.error_code = error_code;
38+
result.error_message = message.empty() ? nullptr : to_c_str(message);
39+
return result;
40+
}
41+
42+
} // namespace
43+
44+
bool native_url_opener_is_supported(void) {
45+
try {
46+
return UrlOpener::IsSupported();
47+
} catch (...) {
48+
return false;
49+
}
50+
}
51+
52+
native_url_open_result_t native_url_opener_open(const char* url) {
53+
if (!url) {
54+
return MakeResult(false, NATIVE_URL_OPEN_ERROR_INVALID_URL_EMPTY, "URL is empty.");
55+
}
56+
57+
try {
58+
UrlOpener opener;
59+
const UrlOpenResult result = opener.Open(std::string(url));
60+
return MakeResult(result.success, ToCErrorCode(result.error_code), result.error_message);
61+
} catch (const std::exception& e) {
62+
return MakeResult(false, NATIVE_URL_OPEN_ERROR_INVOCATION_FAILED, e.what());
63+
} catch (...) {
64+
return MakeResult(false, NATIVE_URL_OPEN_ERROR_INVOCATION_FAILED,
65+
"Unexpected error while opening URL.");
66+
}
67+
}
68+
69+
void native_url_open_result_free(native_url_open_result_t* result) {
70+
if (!result) {
71+
return;
72+
}
73+
74+
if (result->error_message) {
75+
free_c_str(result->error_message);
76+
result->error_message = nullptr;
77+
}
78+
result->success = false;
79+
result->error_code = NATIVE_URL_OPEN_ERROR_NONE;
80+
}

src/capi/url_opener_c.h

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#pragma once
2+
3+
#ifdef __cplusplus
4+
extern "C" {
5+
#endif
6+
7+
#include <stdbool.h>
8+
9+
/**
10+
* @brief Error codes returned by URL opening APIs.
11+
*/
12+
typedef enum {
13+
NATIVE_URL_OPEN_ERROR_NONE = 0,
14+
NATIVE_URL_OPEN_ERROR_INVALID_URL_EMPTY = 1,
15+
NATIVE_URL_OPEN_ERROR_INVALID_URL_MISSING_SCHEME = 2,
16+
NATIVE_URL_OPEN_ERROR_INVALID_URL_UNSUPPORTED_SCHEME = 3,
17+
NATIVE_URL_OPEN_ERROR_UNSUPPORTED_PLATFORM = 4,
18+
NATIVE_URL_OPEN_ERROR_INVOCATION_FAILED = 5,
19+
} native_url_open_error_code_t;
20+
21+
/**
22+
* @brief Result payload for URL open attempts.
23+
*/
24+
typedef struct {
25+
bool success;
26+
native_url_open_error_code_t error_code;
27+
char* error_message;
28+
} native_url_open_result_t;
29+
30+
/**
31+
* @brief Check whether URL opening is supported on this platform.
32+
*/
33+
bool native_url_opener_is_supported(void);
34+
35+
/**
36+
* @brief Attempt to open URL with the system default browser.
37+
*
38+
* Caller must release result.error_message via native_url_open_result_free().
39+
*/
40+
native_url_open_result_t native_url_opener_open(const char* url);
41+
42+
/**
43+
* @brief Free owned memory inside a native_url_open_result_t.
44+
*/
45+
void native_url_open_result_free(native_url_open_result_t* result);
46+
47+
#ifdef __cplusplus
48+
}
49+
#endif
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#include "../../url_opener.h"
2+
#include "../../url_opener_internal.h"
3+
4+
namespace nativeapi {
5+
namespace {
6+
7+
UrlLaunchOutcome LaunchUnsupported(const std::string& url) {
8+
(void)url;
9+
return {false, "URL opening is not implemented on Android in this native layer."};
10+
}
11+
12+
} // namespace
13+
14+
bool UrlOpener::IsSupported() {
15+
return false;
16+
}
17+
18+
UrlOpenResult UrlOpener::Open(const std::string& url) const {
19+
UrlOpenResult result = OpenUrlWithLauncher(url, LaunchUnsupported);
20+
if (!result.success && result.error_code == UrlOpenErrorCode::kInvocationFailed) {
21+
result.error_code = UrlOpenErrorCode::kUnsupportedPlatform;
22+
}
23+
return result;
24+
}
25+
26+
} // namespace nativeapi

src/platform/ios/url_opener_ios.mm

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#import <Foundation/Foundation.h>
2+
#import <UIKit/UIKit.h>
3+
4+
#include <dispatch/dispatch.h>
5+
6+
#include "../../url_opener.h"
7+
#include "../../url_opener_internal.h"
8+
9+
namespace nativeapi {
10+
namespace {
11+
12+
UrlLaunchOutcome LaunchUrlOnMainThread(const std::string& url) {
13+
@autoreleasepool {
14+
UIApplication* app = [UIApplication sharedApplication];
15+
if (!app) {
16+
return {false, "UIApplication is unavailable."};
17+
}
18+
19+
NSString* ns_url = [NSString stringWithUTF8String:url.c_str()];
20+
if (!ns_url) {
21+
return {false, "Failed to build NSURL from UTF-8 input."};
22+
}
23+
24+
NSURL* target = [NSURL URLWithString:ns_url];
25+
if (!target) {
26+
return {false, "Failed to parse URL."};
27+
}
28+
29+
if (![app canOpenURL:target]) {
30+
return {false, "No handler available for URL."};
31+
}
32+
33+
if (@available(iOS 10.0, *)) {
34+
__block BOOL open_result = NO;
35+
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
36+
[app openURL:target
37+
options:@{}
38+
completionHandler:^(BOOL success) {
39+
open_result = success;
40+
dispatch_semaphore_signal(semaphore);
41+
}];
42+
43+
const long wait_result =
44+
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
45+
if (wait_result != 0) {
46+
return {false, "Timed out waiting for openURL completion."};
47+
}
48+
if (!open_result) {
49+
return {false, "UIApplication failed to open URL."};
50+
}
51+
return {true, ""};
52+
}
53+
54+
#pragma clang diagnostic push
55+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
56+
const BOOL opened = [app openURL:target];
57+
#pragma clang diagnostic pop
58+
if (!opened) {
59+
return {false, "UIApplication failed to open URL."};
60+
}
61+
return {true, ""};
62+
}
63+
}
64+
65+
UrlLaunchOutcome LaunchUrl(const std::string& url) {
66+
if ([NSThread isMainThread]) {
67+
return LaunchUrlOnMainThread(url);
68+
}
69+
70+
__block UrlLaunchOutcome outcome;
71+
dispatch_sync(dispatch_get_main_queue(), ^{
72+
outcome = LaunchUrlOnMainThread(url);
73+
});
74+
return outcome;
75+
}
76+
77+
} // namespace
78+
79+
bool UrlOpener::IsSupported() {
80+
return true;
81+
}
82+
83+
UrlOpenResult UrlOpener::Open(const std::string& url) const {
84+
return OpenUrlWithLauncher(url, LaunchUrl);
85+
}
86+
87+
} // namespace nativeapi
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#include <gio/gio.h>
2+
3+
#include "../../url_opener.h"
4+
#include "../../url_opener_internal.h"
5+
6+
namespace nativeapi {
7+
namespace {
8+
9+
UrlLaunchOutcome LaunchUrl(const std::string& url) {
10+
GError* error = nullptr;
11+
const gboolean ok = g_app_info_launch_default_for_uri(url.c_str(), nullptr, &error);
12+
if (!ok) {
13+
std::string message = "Failed to launch URL via desktop defaults.";
14+
if (error && error->message) {
15+
message = error->message;
16+
}
17+
if (error) {
18+
g_error_free(error);
19+
}
20+
return {false, message};
21+
}
22+
return {true, ""};
23+
}
24+
25+
} // namespace
26+
27+
bool UrlOpener::IsSupported() {
28+
return true;
29+
}
30+
31+
UrlOpenResult UrlOpener::Open(const std::string& url) const {
32+
return OpenUrlWithLauncher(url, LaunchUrl);
33+
}
34+
35+
} // namespace nativeapi
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#import <Cocoa/Cocoa.h>
2+
#import <Foundation/Foundation.h>
3+
4+
#include "../../url_opener.h"
5+
#include "../../url_opener_internal.h"
6+
7+
namespace nativeapi {
8+
namespace {
9+
10+
UrlLaunchOutcome LaunchUrl(const std::string& url) {
11+
@autoreleasepool {
12+
NSString* ns_url = [NSString stringWithUTF8String:url.c_str()];
13+
if (!ns_url) {
14+
return {false, "Failed to build NSURL from UTF-8 input."};
15+
}
16+
17+
NSURL* target = [NSURL URLWithString:ns_url];
18+
if (!target) {
19+
return {false, "Failed to parse URL."};
20+
}
21+
22+
const BOOL opened = [[NSWorkspace sharedWorkspace] openURL:target];
23+
if (!opened) {
24+
return {false, "NSWorkspace could not open the URL."};
25+
}
26+
return {true, ""};
27+
}
28+
}
29+
30+
} // namespace
31+
32+
bool UrlOpener::IsSupported() {
33+
return true;
34+
}
35+
36+
UrlOpenResult UrlOpener::Open(const std::string& url) const {
37+
return OpenUrlWithLauncher(url, LaunchUrl);
38+
}
39+
40+
} // namespace nativeapi
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#include "../../url_opener.h"
2+
#include "../../url_opener_internal.h"
3+
4+
namespace nativeapi {
5+
namespace {
6+
7+
UrlLaunchOutcome LaunchUnsupported(const std::string& url) {
8+
(void)url;
9+
return {false, "URL opening is not implemented on OHOS in this native layer."};
10+
}
11+
12+
} // namespace
13+
14+
bool UrlOpener::IsSupported() {
15+
return false;
16+
}
17+
18+
UrlOpenResult UrlOpener::Open(const std::string& url) const {
19+
UrlOpenResult result = OpenUrlWithLauncher(url, LaunchUnsupported);
20+
if (!result.success && result.error_code == UrlOpenErrorCode::kInvocationFailed) {
21+
result.error_code = UrlOpenErrorCode::kUnsupportedPlatform;
22+
}
23+
return result;
24+
}
25+
26+
} // namespace nativeapi

0 commit comments

Comments
 (0)