iOS: Add basic deep link support.

Fixes #21577
This commit is contained in:
Henrik Rydgård committed 2026-04-28 22:17:24 +02:00
1 parent e63c209fa7
commit e0a5a3e2bf
11 files changed
+170 -45

No files matched your search

+5
View File
@@ -2,6 +2,8 @@
#import <UIKit/UIKit.h>
#include <string_view>
@protocol PPSSPPViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@@ -11,5 +13,8 @@
@property (nonatomic, strong) NSDictionary *launchOptions;
- (BOOL)launchPPSSPP:(int)argc argv:(char**)argv;
- (void)processFilePath:(NSString *)path;
@end
void copyDeepLinkForPath(std::string_view filePath);
+65 -2
View File
@@ -5,6 +5,7 @@
#import "Common/System/NativeApp.h"
#import "Core/System.h"
#import "Core/Config.h"
#import "Core/Util/PathUtil.h"
#import "Common/Log.h"
#import "IAPManager.h"
@@ -107,9 +108,48 @@ __attribute__((used)) static Class _forceLinkSceneDelegate = [SceneDelegate clas
- (void)applicationWillTerminate:(UIApplication *)application {
}
// In AppDelegate.mm
- (void)processFilePath:(NSString *)path {
// Convert NSString to std::string
std::string cppPath([path UTF8String]);
// Try to fixup the path, the app directory changes on every bootup.
Path gamePath(cppPath);
TryUpdateSavedPath(&gamePath);
// Call the C++ function to handle the file
System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, gamePath.ToString());
}
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
// 1. Check the scheme
if (![[url scheme] isEqualToString:@"ppsspp"]) {
NSLog(@"PPSSPPUIApplication.openURL called with invalid URL: %@", url);
return NO;
}
NSLog(@"PPSSPPUIApplication.openURL called with valid URL: %@", url);
// 2. Parse the URL (using NSURLComponents is safer than manual string parsing)
NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
NSArray *queryItems = [components queryItems];
NSLog(@"PPSSPPUIApplication.openURL query items: %@", queryItems);
for (NSURLQueryItem *item in queryItems) {
if ([item.name isEqualToString:@"path"]) {
NSString *receivedPath = item.value;
[self processFilePath:receivedPath];
} else {
NSLog(@"PPSSPPUIApplication.openURL: Unrecognized query item: %@=%@", item.name, item.value);
}
}
return YES;
}
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
switch (g_Config.iScreenRotation) {
case ROTATION_LOCKED_HORIZONTAL:
switch (g_Config.iScreenRotation) {
case ROTATION_LOCKED_HORIZONTAL:
return UIInterfaceOrientationMaskLandscapeRight;
case ROTATION_LOCKED_VERTICAL:
case ROTATION_LOCKED_VERTICAL180:
@@ -132,3 +172,26 @@ __attribute__((used)) static Class _forceLinkSceneDelegate = [SceneDelegate clas
}
@end
void copyDeepLinkForPath(std::string_view filePath) {
// 1. Convert std::string_view to NSString
// We use the length and data to avoid issues with null terminators
NSString *pathString = [[NSString alloc] initWithBytes:filePath.data()
length:filePath.length()
encoding:NSUTF8StringEncoding];
// 2. Percent-encode the path
// This handles spaces, slashes, and special characters for the URL query
NSCharacterSet *allowedChars = [NSCharacterSet URLQueryAllowedCharacterSet];
NSString *encodedPath = [pathString stringByAddingPercentEncodingWithAllowedCharacters:allowedChars];
// 3. Construct the full URL
// Format: scheme://host?query
NSString *deepLink = [NSString stringWithFormat:@"ppsspp://open?path=%@", encodedPath];
// 4. Copy to the iOS System Clipboard (UIPasteboard)
[UIPasteboard generalPasteboard].string = deepLink;
// Optional: Log it for debugging
NSLog(@"Deep link copied: %@", deepLink);
}
+11
View File
@@ -40,6 +40,17 @@
<string>AppIcon</string>
</dict>
</dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>org.ppsspp.ppsspp-free</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ppsspp</string>
</array>
</dict>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
+31 -31
View File
@@ -70,48 +70,48 @@
#if !PPSSPP_PLATFORM(IOS_APP_STORE)
- (void)decodeKeyEvent:(NSInteger *)eventMem {
NSInteger eventType = eventMem[GSEVENT_TYPE];
NSInteger eventScanCode = eventMem[GSEVENTKEY_KEYCODE];
NSInteger eventType = eventMem[GSEVENT_TYPE];
NSInteger eventScanCode = eventMem[GSEVENTKEY_KEYCODE];
//NSLog(@"Got key: %d", (int)eventScanCode);
//NSLog(@"Got key: %d", (int)eventScanCode);
if (eventType == GSEVENT_TYPE_KEYUP) {
struct KeyInput key;
key.flags = KeyInputFlags::UP;
key.keyCode = getSmartKeyboardMap((int)eventScanCode);
key.deviceId = DEVICE_ID_KEYBOARD;
NativeKey(key);
} else if (GSEVENT_TYPE_KEYDOWN) {
struct KeyInput key;
key.flags = KeyInputFlags::DOWN;
key.keyCode = getSmartKeyboardMap((int)eventScanCode);
key.deviceId = DEVICE_ID_KEYBOARD;
NativeKey(key);
}
if (eventType == GSEVENT_TYPE_KEYUP) {
struct KeyInput key;
key.flags = KeyInputFlags::UP;
key.keyCode = getSmartKeyboardMap((int)eventScanCode);
key.deviceId = DEVICE_ID_KEYBOARD;
NativeKey(key);
} else if (GSEVENT_TYPE_KEYDOWN) {
struct KeyInput key;
key.flags = KeyInputFlags::DOWN;
key.keyCode = getSmartKeyboardMap((int)eventScanCode);
key.deviceId = DEVICE_ID_KEYBOARD;
NativeKey(key);
}
}
- (void)handleKeyUIEvent:(UIEvent *) event {
if ([event respondsToSelector:@selector(_gsEvent)]) {
NSInteger *eventMem;
if ([event respondsToSelector:@selector(_gsEvent)]) {
NSInteger *eventMem;
eventMem = (NSInteger *) (__bridge void*)[event performSelector:@selector(_gsEvent)];
if (eventMem) {
[self decodeKeyEvent:eventMem];
}
}
eventMem = (NSInteger *) (__bridge void*)[event performSelector:@selector(_gsEvent)];
if (eventMem) {
[self decodeKeyEvent:eventMem];
}
}
}
- (void)sendEvent:(UIEvent *)event {
[super sendEvent:event];
if ([event respondsToSelector:@selector(_gsEvent)]) {
NSInteger *eventMem;
[super sendEvent:event];
if ([event respondsToSelector:@selector(_gsEvent)]) {
NSInteger *eventMem;
eventMem = (NSInteger *) (__bridge void*)[event performSelector:@selector(_gsEvent)];
if (eventMem) {
[self decodeKeyEvent:eventMem];
}
}
eventMem = (NSInteger *) (__bridge void*)[event performSelector:@selector(_gsEvent)];
if (eventMem) {
[self decodeKeyEvent:eventMem];
}
}
}
#endif // !IOS_APP_STORE
+18
View File
@@ -95,6 +95,24 @@
[sharedViewController didBecomeActive];
}
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
for (UIOpenURLContext *context in URLContexts) {
NSLog(@"SceneDelegate: openURLContexts called with URL: %@", context.URL);
NSURL *url = context.URL;
if (![[url scheme] isEqualToString:@"ppsspp"]) {
NSLog(@"SceneDelegate: ignoring URL with unknown scheme: %@", url);
continue;
}
NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
for (NSURLQueryItem *item in components.queryItems) {
if ([item.name isEqualToString:@"path"]) {
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
[appDelegate processFilePath:item.value];
}
}
}
}
- (void)sceneWillResignActive:(UIScene *)scene {
INFO_LOG(Log::G3D, "sceneWillResignActive");
+13 -11
View File
@@ -83,7 +83,7 @@ static void *exception_handler(void *argument) {
static NSDictionary *parse_entitlements(const void *entitlements, size_t length) {
char *copy = (char *)malloc(length);
memcpy(copy, entitlements, length);
// strip out psychic paper entitlement hiding
if (@available(iOS 13.5, *)) {
} else {
@@ -95,7 +95,7 @@ static NSDictionary *parse_entitlements(const void *entitlements, size_t length)
}
NSData *data = [NSData dataWithBytes:copy length:length];
free(copy);
return [NSPropertyListSerialization propertyListWithData:data
options:NSPropertyListImmutable
format:nil
@@ -104,7 +104,7 @@ static NSDictionary *parse_entitlements(const void *entitlements, size_t length)
static NSDictionary *app_entitlements(void) {
// Inspired by codesign.c in Darwin sources for Security.framework
// Find our mach-o header
Dl_info dl_info;
if (dladdr((const void *)app_entitlements, &dl_info) == 0)
@@ -115,7 +115,7 @@ static NSDictionary *app_entitlements(void) {
struct mach_header_64 *header = (struct mach_header_64 *)dl_info.dli_fbase;
if (header->magic != MH_MAGIC_64)
return nil;
// Simulator executables have fake entitlements in the code signature. The real entitlements can be found in an __entitlements section.
size_t entitlements_size;
uint8_t *entitlements_data = getsectiondata(header, "__TEXT", "__entitlements", &entitlements_size);
@@ -128,7 +128,7 @@ static NSDictionary *app_entitlements(void) {
format:nil
error:nil];
}
// Find the LC_CODE_SIGNATURE
struct load_command *lc = (struct load_command *) (base + sizeof(*header));
struct linkedit_data_command *cs_lc = NULL;
@@ -152,7 +152,7 @@ static NSDictionary *app_entitlements(void) {
const struct cs_superblob *cs = (const struct cs_superblob *)csData.bytes;
if (ntohl(cs->magic) != 0xfade0cc0)
return nil;
// Find the entitlements in the code signature
for (uint32_t i = 0; i < ntohl(cs->count); i++) {
struct cs_entitlements *ents = (struct cs_entitlements *) ((char *) cs + ntohl(cs->index[i].offset));
@@ -180,7 +180,7 @@ static char *childArgv[] = {NULL, "debugme", NULL};
bool jb_spawn_ptrace_child(int argc, char **argv) {
int ret; pid_t pid;
if (argc > 1 && strcmp(argv[1], childArgv[1]) == 0) {
ret = ptrace(PT_TRACE_ME, 0, NULL, 0);
NSLog(@"child: ptrace(PT_TRACE_ME) %d", ret);
@@ -250,7 +250,7 @@ static bool jb_has_debugger_attached(void) {
bool jb_enable_ptrace_hack(void) {
bool debugged = jb_has_debugger_attached();
// Thanks to this comment: https://news.ycombinator.com/item?id=18431524
// We use this hack to allow mmap with PROT_EXEC (which usually requires the
// dynamic-codesigning entitlement) by tricking the process into thinking
@@ -259,7 +259,7 @@ bool jb_enable_ptrace_hack(void) {
if (ptrace(PT_TRACE_ME, 0, NULL, 0) < 0) {
return false;
}
// ptracing ourselves confuses the kernel and will cause bad things to
// happen to the system (hangs…) if an exception or signal occurs. Setup
// some "safety nets" so we can cause the process to exit in a somewhat sane
@@ -269,7 +269,7 @@ bool jb_enable_ptrace_hack(void) {
if (!debugged) {
// First, ensure that signals are delivered as Mach software exceptions…
ptrace(PT_SIGEXC, 0, NULL, 0);
// …then ensure that this exception goes through our exception handler.
// I think it's OK to just watch for EXC_SOFTWARE because the other
// exceptions (e.g. EXC_BAD_ACCESS, EXC_BAD_INSTRUCTION, and friends)
@@ -282,7 +282,7 @@ bool jb_enable_ptrace_hack(void) {
pthread_t thread;
pthread_create(&thread, NULL, exception_handler, (void *)&port);
}
return true;
}
#endif
@@ -358,6 +358,8 @@ float System_GetPropertyFloat(SystemProperty prop) {
bool System_GetPropertyBool(SystemProperty prop) {
switch (prop) {
case SYSPROP_HAS_DEEP_LINKS:
return true;
case SYSPROP_DISPLAY_HAS_CAMERA_CUTOUT:
return true;
case SYSPROP_HAS_FILE_BROWSER: