diff --git a/Common/Net/URL.h b/Common/Net/URL.h index 9b3b96f7bf..20b296140a 100644 --- a/Common/Net/URL.h +++ b/Common/Net/URL.h @@ -189,6 +189,5 @@ private: int port_; }; - std::string UriDecode(std::string_view sSrc); std::string UriEncode(std::string_view sSrc); diff --git a/Common/System/System.h b/Common/System/System.h index e58a5ccc85..38d9c2a0e3 100644 --- a/Common/System/System.h +++ b/Common/System/System.h @@ -168,6 +168,7 @@ enum SystemProperty { SYSPROP_HAS_LOGIN_DIALOG, SYSPROP_HAS_TEXT_CLIPBOARD, SYSPROP_HAS_TEXT_INPUT_DIALOG, // Indicates that System_InputBoxGetString is available. + SYSPROP_HAS_DEEP_LINKS, // ios-style deep links SYSPROP_CAN_CREATE_SHORTCUT, SYSPROP_CAN_SHOW_FILE, diff --git a/Core/HW/MemoryStick.cpp b/Core/HW/MemoryStick.cpp index 6c6d9a4d66..b3ba0ebfc6 100644 --- a/Core/HW/MemoryStick.cpp +++ b/Core/HW/MemoryStick.cpp @@ -167,6 +167,8 @@ void MemoryStick_SetState(MemStickState state) { return; } + INFO_LOG(Log::System, "Memory stick state changed: %d -> %d", memStickState, state); + memStickState = state; // If removed, we unmount. Otherwise, mounting is delayed. diff --git a/UI/GameScreen.cpp b/UI/GameScreen.cpp index ae164ff823..cd46f2fbce 100644 --- a/UI/GameScreen.cpp +++ b/UI/GameScreen.cpp @@ -57,6 +57,14 @@ #include "UI/SavedataScreen.h" #include "UI/MiscViews.h" +// This is in an objective-C file. +#if PPSSPP_PLATFORM(IOS) +void copyDeepLinkForPath(std::string_view filePath); +#else +// dummy +void copyDeepLinkForPath(std::string_view) {} +#endif + constexpr GameInfoFlags g_desiredFlags = GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::PIC0 | GameInfoFlags::PIC1 | GameInfoFlags::ICON1_PMF | GameInfoFlags::UNCOMPRESSED_SIZE | GameInfoFlags::SIZE | GameInfoFlags::SAVEDATA_SIZE; class PMFView : public UI::InertView { @@ -561,6 +569,17 @@ void GameScreen::CreateContextMenu(UI::ViewGroup *parent) { }); } + if (System_GetPropertyBool(SYSPROP_HAS_DEEP_LINKS)) { + Choice *btnCopyDeepLink = parent->Add(new Choice(di->T("Copy deep link"), ImageID("I_FILE_COPY"))); + btnCopyDeepLink->OnClick.Add([this](UI::EventParams &e) { + auto di = GetI18NCategory(I18NCat::DIALOG); + const std::string deepLink = gamePath_.ToString(); + copyDeepLinkForPath(deepLink); + // Success indication. Not worth a translatable string. + g_OSD.Show(OSDType::MESSAGE_INFO, ApplySafeSubstitutions(di->T("Copied to clipboard: %1"), deepLink), 0.0f, "copyToClip"); + }); + } + if (info_->saveDataSize) { Choice *btnDeleteSaveData = new Choice(ga->T("Delete Save Data"), ImageID("I_TRASHCAN")); parent->Add(btnDeleteSaveData)->OnClick.Handle(this, &GameScreen::OnDeleteSaveData); diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index 79b1c55b58..849c5ccb48 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -71,6 +71,11 @@ #include "UI/MiscViews.h" #include "UI/AdhocServerScreen.h" +// This is in an objective-C file. +#if PPSSPP_PLATFORM(IOS) +void copyDeepLinkForPath(std::string_view filePath); +#endif + static void AfterSaveStateAction(SaveState::Status status, std::string_view message) { if (!message.empty() && (!g_Config.bDumpFrames || !g_Config.bDumpVideoOutput)) { g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, diff --git a/ios/AppDelegate.h b/ios/AppDelegate.h index 3e48c9bfda..6d81b8b348 100644 --- a/ios/AppDelegate.h +++ b/ios/AppDelegate.h @@ -2,6 +2,8 @@ #import +#include + @protocol PPSSPPViewController; @interface AppDelegate : UIResponder @@ -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); diff --git a/ios/AppDelegate.mm b/ios/AppDelegate.mm index c6b00767b9..b2bfc9c434 100644 --- a/ios/AppDelegate.mm +++ b/ios/AppDelegate.mm @@ -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 *)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); +} diff --git a/ios/PPSSPP-Info.plist b/ios/PPSSPP-Info.plist index f977b05b70..65fb5f77d3 100644 --- a/ios/PPSSPP-Info.plist +++ b/ios/PPSSPP-Info.plist @@ -40,6 +40,17 @@ AppIcon + CFBundleURLTypes + + + CFBundleURLName + org.ppsspp.ppsspp-free + CFBundleURLSchemes + + ppsspp + + + LSRequiresIPhoneOS UIViewControllerBasedStatusBarAppearance diff --git a/ios/PPSSPPUIApplication.mm b/ios/PPSSPPUIApplication.mm index be3c76e444..c973ed0dc3 100644 --- a/ios/PPSSPPUIApplication.mm +++ b/ios/PPSSPPUIApplication.mm @@ -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 diff --git a/ios/SceneDelegate.mm b/ios/SceneDelegate.mm index d55e59c2c0..d7aaf5e0f4 100644 --- a/ios/SceneDelegate.mm +++ b/ios/SceneDelegate.mm @@ -95,6 +95,24 @@ [sharedViewController didBecomeActive]; } +- (void)scene:(UIScene *)scene openURLContexts:(NSSet *)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"); diff --git a/ios/main.mm b/ios/main.mm index 0407b8bd35..37e32d9653 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -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: