From 5f261bd7ffb71b50dc6dc09108a5bb2ad6a27bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 22 Sep 2026 17:17:12 -0600 Subject: [PATCH] Utility: Stand in for the HtmlViewer, offering to open the page in a browser Instead of the PSP's web browser, a dialog shows the URL the game wants and opens it in the host's browser on X, or backs out on O. Either way the game sees the browser closed normally. Platforms that can't open a URL (the new SYSPROP_CAN_LAUNCH_URL) only offer to back out. Only plain printable-ASCII http(s) addresses are handed over, and on Linux without a shell. What the firmware does (sceUtility_Driver, 6.61, plus utility/dialog/htmlviewer): the HtmlViewer has its own state apart from the other dialogs, so they don't block each other, and its calls return WRONG_TYPE until one has started. The request size picks the 2.00 to 3.00 layout, and InitStart allocates 3.5MB of user memory (4.5MB with options bit 0x400 from 2.70 on), failing with 800200d9 without it. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/System/System.h | 1 + Core/CMakeLists.txt | 4 +- Core/Core.vcxproj | 2 + Core/Core.vcxproj.filters | 6 + Core/Dialog/PSPDialog.cpp | 5 +- Core/Dialog/PSPDialog.h | 4 + Core/Dialog/PSPHtmlViewerDialog.cpp | 229 ++++++++++++++++++++++++++++ Core/Dialog/PSPHtmlViewerDialog.h | 60 ++++++++ Core/HLE/ErrorCodes.h | 1 + Core/HLE/sceUtility.cpp | 45 +++++- Core/HLE/sceUtility.h | 1 + SDL/SDLMain.cpp | 11 +- UWP/CoreUWP/CoreUWP.vcxproj | 2 + UWP/CoreUWP/CoreUWP.vcxproj.filters | 2 + UWP/PPSSPP_UWPMain.cpp | 2 + Windows/main.cpp | 1 + android/jni/Android.mk | 1 + android/jni/app-android.cpp | 2 + ios/main.mm | 2 + libretro/Makefile.common | 1 + libretro/libretro.cpp | 1 + test.py | 1 + 22 files changed, 373 insertions(+), 11 deletions(-) create mode 100644 Core/Dialog/PSPHtmlViewerDialog.cpp create mode 100644 Core/Dialog/PSPHtmlViewerDialog.h diff --git a/Common/System/System.h b/Common/System/System.h index d2ce08d675..3016c4d7e6 100644 --- a/Common/System/System.h +++ b/Common/System/System.h @@ -170,6 +170,7 @@ enum SystemProperty { SYSPROP_CAN_CREATE_SHORTCUT, SYSPROP_CAN_SHOW_FILE, + SYSPROP_CAN_LAUNCH_URL, // System_LaunchUrl can open a web page. SYSPROP_SUPPORTS_HTTPS, diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index 8148f28d09..556c6a7221 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -209,8 +209,6 @@ if(NOT LIBRETRO) Debugger/WebSocket/SteppingSubscriber.h Debugger/WebSocket/WebSocketUtils.cpp Debugger/WebSocket/WebSocketUtils.h - Dialog/PSPPlaceholderDialog.cpp - Dialog/PSPPlaceholderDialog.h HW/GranularMixer.cpp HW/GranularMixer.h HW/StereoResampler.cpp @@ -356,7 +354,9 @@ add_library(Core STATIC Dialog/PSPNetconfDialog.cpp Dialog/PSPNetconfDialog.h Dialog/PSPNpSigninDialog.cpp + Dialog/PSPHtmlViewerDialog.cpp Dialog/PSPNpSigninDialog.h + Dialog/PSPHtmlViewerDialog.h Dialog/PSPOskDialog.cpp Dialog/PSPOskDialog.h Dialog/PSPOskConstants.cpp diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index afc31a0f5f..58f6c8b408 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -569,6 +569,7 @@ + @@ -1087,6 +1088,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 8baa918c5e..4e8a93738b 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -556,6 +556,9 @@ Dialog + + Dialog + Debugger @@ -1881,6 +1884,9 @@ Dialog + + Dialog + MIPS\JitCommon diff --git a/Core/Dialog/PSPDialog.cpp b/Core/Dialog/PSPDialog.cpp index 6ca7a64bfb..ac554c6e4f 100644 --- a/Core/Dialog/PSPDialog.cpp +++ b/Core/Dialog/PSPDialog.cpp @@ -50,6 +50,7 @@ const char *UtilityDialogTypeToString(UtilityDialogType type) { case UtilityDialogType::GAMESHARING: return "GAMESHARING"; case UtilityDialogType::GAMEDATAINSTALL: return "GAMEDATAINSTALL"; case UtilityDialogType::NPSIGNIN: return "NPSIGNIN"; + case UtilityDialogType::HTMLVIEWER: return "HTMLVIEWER"; default: return "(unknown)"; } } @@ -101,7 +102,7 @@ void PSPDialog::UpdatePendingStatus() { if (pendingStatus == SCE_UTILITY_STATUS_NONE && status == SCE_UTILITY_STATUS_SHUTDOWN) { FinishVolatile(); } else if (pendingStatus == SCE_UTILITY_STATUS_RUNNING && status == SCE_UTILITY_STATUS_INITIALIZE) { - if (!volatileLocked_) { + if (!volatileLocked_ && LocksVolatileMemory()) { volatileLocked_ = KernelVolatileMemLock(0, 0, 0) == 0; changeAllowed = volatileLocked_; } @@ -149,7 +150,7 @@ void PSPDialog::ChangeStatus(DialogStatus newStatus, int delayUs) { if (newStatus == SCE_UTILITY_STATUS_NONE && status == SCE_UTILITY_STATUS_SHUTDOWN) { FinishVolatile(); } else if (newStatus == SCE_UTILITY_STATUS_RUNNING && status == SCE_UTILITY_STATUS_INITIALIZE) { - if (!volatileLocked_) { + if (!volatileLocked_ && LocksVolatileMemory()) { // TODO: Should probably make the status pending instead? volatileLocked_ = KernelVolatileMemLock(0, 0, 0) == 0; } diff --git a/Core/Dialog/PSPDialog.h b/Core/Dialog/PSPDialog.h index 0380d7a548..9f0320305e 100644 --- a/Core/Dialog/PSPDialog.h +++ b/Core/Dialog/PSPDialog.h @@ -108,6 +108,10 @@ protected: // TODO: Remove this once all dialogs are updated. virtual bool UseAutoStatus() = 0; + // Whether the dialog holds volatile memory while running. + virtual bool LocksVolatileMemory() const { + return true; + } static int GetConfirmButton(); static int GetCancelButton(); diff --git a/Core/Dialog/PSPHtmlViewerDialog.cpp b/Core/Dialog/PSPHtmlViewerDialog.cpp new file mode 100644 index 0000000000..7f83874e54 --- /dev/null +++ b/Core/Dialog/PSPHtmlViewerDialog.cpp @@ -0,0 +1,229 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include "Common/Data/Text/I18n.h" +#include "Common/Serialize/Serializer.h" +#include "Common/Serialize/SerializeFuncs.h" +#include "Common/StringUtils.h" +#include "Common/System/System.h" +#include "Core/Dialog/PSPHtmlViewerDialog.h" +#include "Core/HLE/ErrorCodes.h" +#include "Core/HLE/sceCtrl.h" +#include "Core/HLE/sceKernelMemory.h" +#include "Core/Util/PPGeDraw.h" + +// Guesses. The real browser takes much longer to come up. +static const int HTMLVIEWER_INIT_DELAY_US = 200000; +static const int HTMLVIEWER_SHUTDOWN_DELAY_US = 40000; + +struct SceUtilityHtmlViewerParam { + pspUtilityDialogCommon common; + u32_le memAddr; + u32_le memSize; + s32_le unknown1; + s32_le unknown2; + u32_le initialUrl; + u32_le numTabs; + u32_le interfaceMode; + u32_le options; + u32_le dlDirName; + u32_le dlFileName; + u32_le ulDirName; + u32_le ulFileName; + u32_le cookieMode; + u32_le unknown3; + u32_le homeUrl; + u32_le textSize; + // The smallest request (0x70) ends here. +}; + +// From sceUtility_Driver's InitStart for the HtmlViewer, firmware 6.61: the request size says which +// firmware's layout it is, and the working memory it allocates for the browser depends on that and +// on bit 0x400 of options. Any other size is INVALID_PARAM_SIZE. +static bool HtmlViewerWorkSize(u32 size, u32 options, u32 *workSize) { + switch (size) { + case 0x70: // 2.00 + case 0x78: // 2.50 + case 0x80: // 2.60 + *workSize = 0x380000; + return true; + case 0x98: // 2.70 + case 0xA4: // 2.80 + case 0xA8: // 3.00 + *workSize = (options & 0x400) ? 0x480000 : 0x380000; + return true; + default: + return false; + } +} + +static std::string ReadUrl(u32 addr) { + if (!Memory::IsValidNullTerminatedString(addr)) { + return ""; + } + return Memory::GetCharPointerUnchecked(addr); +} + +// Only plain web addresses get handed to the host: printable ASCII, nothing a shell or URL parser +// could take for something else. +static bool IsWebUrl(std::string_view url) { + if (!startsWithNoCase(url, "http://") && !startsWithNoCase(url, "https://")) { + return false; + } + for (char c : url) { + if (c <= 0x20 || c >= 0x7F) { + return false; + } + } + return true; +} + +PSPHtmlViewerDialog::PSPHtmlViewerDialog(UtilityDialogType type) : PSPDialog(type) { +} + +int PSPHtmlViewerDialog::Init(u32 paramAddr) { + if (GetStatus() != SCE_UTILITY_STATUS_NONE) { + return SCE_ERROR_UTILITY_INVALID_STATUS; + } + if (!Memory::IsValidRange(paramAddr, sizeof(pspUtilityDialogCommon))) { + return SCE_ERROR_UTILITY_INVALID_ADDRESS; + } + const u32 size = Memory::ReadUnchecked_U32(paramAddr); + const SceUtilityHtmlViewerParam *param = (const SceUtilityHtmlViewerParam *)Memory::GetPointerUnchecked(paramAddr); + u32 workSize = 0; + if (!HtmlViewerWorkSize(size, param->options, &workSize)) { + return SCE_ERROR_UTILITY_INVALID_PARAM_SIZE; + } + if (!Memory::IsValidRange(paramAddr, size)) { + return SCE_ERROR_UTILITY_INVALID_ADDRESS; + } + + u32 addr = userMemory.Alloc(workSize, false, "HtmlViewer"); + if (addr == (u32)-1) { + return SCE_KERNEL_ERROR_MEMBLOCK_ALLOC_FAILED; + } + workMemory_ = addr; + + params_ = paramAddr; + url_ = ReadUrl(param->initialUrl); + if (url_.empty()) { + url_ = ReadUrl(param->homeUrl); + } + started_ = true; + scrollPos_ = 0.0f; + framesUpHeld_ = 0; + framesDownHeld_ = 0; + + ChangeStatus(SCE_UTILITY_STATUS_INITIALIZE, 0); + ChangeStatus(SCE_UTILITY_STATUS_RUNNING, HTMLVIEWER_INIT_DELAY_US); + InitCommon(); + // Presses from before (like the one that opened this) mustn't open the page. + UpdateButtons(); + StartFade(true); + return 0; +} + +int PSPHtmlViewerDialog::Update(int animSpeed) { + if (GetStatus() != SCE_UTILITY_STATUS_RUNNING) { + return SCE_ERROR_UTILITY_INVALID_STATUS; + } + + auto di = GetI18NCategory(I18NCat::DIALOG); + const bool hasUrl = IsWebUrl(url_); + const bool canOpen = hasUrl && System_GetPropertyBool(SYSPROP_CAN_LAUNCH_URL); + + UpdateButtons(); + UpdateCommon(); + UpdateFade(animSpeed); + + StartDraw(); + PPGeDrawRect(0, 0, 480, 272, CalcFadedColor(0xC0C8B2AC)); + if (hasUrl) { + DisplayMessage2(std::string(di->T("The game wants to open a web page:")) + "\n\n" + url_); + } else { + DisplayMessage2(di->T("The game wants to open a web page, but gave no address.")); + } + + // Fixed buttons, whatever the game's confirm button is: X opens, O backs out. + PPGeStyle textStyle = FadedStyle(PPGeAlign::BOX_LEFT, 0.55f); + if (canOpen) { + PPGeDrawImage(ImageID("I_CROSS"), 183.5f, 256, 11.5f, 11.5f, textStyle); + PPGeDrawText(di->T("Open in browser"), 183.5f + 14.5f, 252, textStyle); + } + PPGeDrawImage(ImageID("I_CIRCLE"), 321.5f, 256, 11.5f, 11.5f, textStyle); + PPGeDrawText(di->T("Back"), 321.5f + 14.5f, 252, textStyle); + + bool done = false; + if (canOpen && IsButtonPressed(CTRL_CROSS)) { + System_LaunchUrl(LaunchUrlType::BROWSER_URL, url_); + done = true; + } else if (IsButtonPressed(CTRL_CIRCLE)) { + done = true; + } + if (done) { + // As if the player had browsed and then left the browser. + params_->result = 0; + params_.NotifyWrite("DialogResult"); + StartFade(false); + } + + EndDraw(); + return 0; +} + +void PSPHtmlViewerDialog::FreeWorkMemory() { + if (workMemory_ != 0) { + userMemory.Free(workMemory_); + workMemory_ = 0; + } +} + +int PSPHtmlViewerDialog::Shutdown(bool force) { + if (ReadStatus() != SCE_UTILITY_STATUS_FINISHED && !force) { + return SCE_ERROR_UTILITY_INVALID_STATUS; + } + + if (force) { + // Only when the emulator shuts down, and user memory goes with it. + workMemory_ = 0; + PSPDialog::Shutdown(true); + } else { + FreeWorkMemory(); + ChangeStatus(SCE_UTILITY_STATUS_SHUTDOWN, 0); + ChangeStatus(SCE_UTILITY_STATUS_NONE, HTMLVIEWER_SHUTDOWN_DELAY_US); + } + return 0; +} + +void PSPHtmlViewerDialog::DoState(PointerWrap &p) { + PSPDialog::DoState(p); + + auto s = p.Section("PSPHtmlViewerDialog", 1); + if (!s) + return; + + Do(p, params_); + Do(p, url_); + Do(p, workMemory_); + Do(p, started_); +} + +pspUtilityDialogCommon *PSPHtmlViewerDialog::GetCommonParam() { + if (params_.IsValid()) + return params_; + return nullptr; +} diff --git a/Core/Dialog/PSPHtmlViewerDialog.h b/Core/Dialog/PSPHtmlViewerDialog.h new file mode 100644 index 0000000000..aa57f54ac4 --- /dev/null +++ b/Core/Dialog/PSPHtmlViewerDialog.h @@ -0,0 +1,60 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include + +#include "Core/Dialog/PSPDialog.h" +#include "Core/MemMap.h" + +// Stands in for the PSP's web browser: shows the URL the game wants to open and offers to open it in +// the host's browser instead. +// +// On a PSP the HtmlViewer has its own state, apart from the other dialogs: it doesn't wait for them, +// and they don't wait for it (utility/dialog/htmlviewer). +class PSPHtmlViewerDialog : public PSPDialog { +public: + PSPHtmlViewerDialog(UtilityDialogType type); + + int Init(u32 paramAddr); + int Update(int animSpeed) override; + int Shutdown(bool force = false) override; + void DoState(PointerWrap &p) override; + pspUtilityDialogCommon *GetCommonParam() override; + + // Until one has started, all the HtmlViewer calls return WRONG_TYPE. + bool HasStarted() const { + return started_; + } + +protected: + bool UseAutoStatus() override { + return false; + } + bool LocksVolatileMemory() const override { + return false; + } + +private: + void FreeWorkMemory(); + + PSPPointer params_; + std::string url_; + u32 workMemory_ = 0; + bool started_ = false; +}; diff --git a/Core/HLE/ErrorCodes.h b/Core/HLE/ErrorCodes.h index 9d345ed2de..f15d353d5f 100644 --- a/Core/HLE/ErrorCodes.h +++ b/Core/HLE/ErrorCodes.h @@ -306,6 +306,7 @@ enum PSPErrorCode : u32 { SCE_ERROR_AV_MODULE_NOT_LOADED = 0x80110F03, SCE_ERROR_UTILITY_INVALID_STATUS = 0x80110001, + SCE_ERROR_UTILITY_INVALID_ADDRESS = 0x80110002, SCE_ERROR_UTILITY_INVALID_PARAM_SIZE = 0x80110004, SCE_ERROR_UTILITY_WRONG_TYPE = 0x80110005, SCE_ERROR_UTILITY_STRING_TOO_LONG = 0x80110102, diff --git a/Core/HLE/sceUtility.cpp b/Core/HLE/sceUtility.cpp index 9356d32f59..67251a22a6 100644 --- a/Core/HLE/sceUtility.cpp +++ b/Core/HLE/sceUtility.cpp @@ -55,6 +55,7 @@ #include "Core/Dialog/PSPDialog.h" #include "Core/Dialog/PSPSaveDialog.h" #include "Core/Dialog/PSPMsgDialog.h" +#include "Core/Dialog/PSPHtmlViewerDialog.h" #include "Core/Dialog/PSPPlaceholderDialog.h" #include "Core/Dialog/PSPOskDialog.h" #include "Core/Dialog/PSPGamedataInstallDialog.h" @@ -327,6 +328,8 @@ static PSPScreenshotDialog *screenshotDialog; static PSPGamedataInstallDialog *gamedataInstallDialog; static PSPNpSigninDialog *npSigninDialog; static PSPPlaceholderDialog *gameSharingDialog; +// Not one of the dialogs above: it has its own state on a PSP, and doesn't count as the current dialog. +static PSPHtmlViewerDialog *htmlViewerDialog; // A lot of state seems to be shared between the various dialog types. static int oldStatus = -1; @@ -400,6 +403,8 @@ static PSPDialog *CurrentDialog(UtilityDialogType type) { return gamedataInstallDialog; case UtilityDialogType::NPSIGNIN: return npSigninDialog; + case UtilityDialogType::HTMLVIEWER: + return htmlViewerDialog; } return nullptr; } @@ -522,6 +527,7 @@ void __UtilityInit() { gamedataInstallDialog = new PSPGamedataInstallDialog(UtilityDialogType::GAMEDATAINSTALL); npSigninDialog = new PSPNpSigninDialog(UtilityDialogType::NPSIGNIN); gameSharingDialog = new PSPPlaceholderDialog(UtilityDialogType::GAMESHARING); + htmlViewerDialog = new PSPHtmlViewerDialog(UtilityDialogType::HTMLVIEWER); currentDialogType = UtilityDialogType::NONE; DeactivateDialog(); @@ -534,7 +540,7 @@ void __UtilityInit() { } void __UtilityDoState(PointerWrap &p) { - auto s = p.Section("sceUtility", 1, 8); + auto s = p.Section("sceUtility", 1, 9); if (!s) { return; } @@ -608,6 +614,9 @@ void __UtilityDoState(PointerWrap &p) { if (s >= 8) { gameSharingDialog->DoState(p); } + if (s >= 9) { + htmlViewerDialog->DoState(p); + } if (!hasAccessThread && accessThread) { accessThread->Forget(); @@ -626,6 +635,7 @@ void __UtilityShutdown() { gamedataInstallDialog->Shutdown(true); npSigninDialog->Shutdown(true); gameSharingDialog->Shutdown(true); + htmlViewerDialog->Shutdown(true); if (accessThread) { // Don't need to free it during shutdown, may have already been freed. @@ -645,6 +655,7 @@ void __UtilityShutdown() { delete gamedataInstallDialog; delete npSigninDialog; delete gameSharingDialog; + delete htmlViewerDialog; } // On a PSP, dialog init and shutdown happen partly at the accessThread priority and partly at the @@ -1677,9 +1688,31 @@ static int sceUtilityGameSharingGetStatus() { return hleLogVerbose(Log::sceUtility, status, "status: %s", UtilityDialogStatusToString(status)); } -// We never run an HtmlViewer, so it's never the current type. +// The HtmlViewer keeps its own state on a PSP: other dialogs don't make it busy or change its type, +// and until one has been started its calls return WRONG_TYPE (utility/dialog/htmlviewer). +static int sceUtilityHtmlViewerInitStart(u32 paramsPtr) { + return hleLogInfo(Log::sceUtility, htmlViewerDialog->Init(paramsPtr)); +} + +static int sceUtilityHtmlViewerShutdownStart() { + if (!htmlViewerDialog->HasStarted()) { + return hleLogDebug(Log::sceUtility, SCE_ERROR_UTILITY_WRONG_TYPE, "wrong dialog type"); + } + return hleLogDebug(Log::sceUtility, htmlViewerDialog->Shutdown()); +} + +static int sceUtilityHtmlViewerUpdate(int animSpeed) { + if (!htmlViewerDialog->HasStarted()) { + return hleLogDebug(Log::sceUtility, SCE_ERROR_UTILITY_WRONG_TYPE, "wrong dialog type"); + } + return hleLogDebug(Log::sceUtility, htmlViewerDialog->Update(animSpeed)); +} + static int sceUtilityHtmlViewerGetStatus() { - return hleLogDebug(Log::sceUtility, SCE_ERROR_UTILITY_WRONG_TYPE, "wrong dialog type"); + if (!htmlViewerDialog->HasStarted()) { + return hleLogDebug(Log::sceUtility, SCE_ERROR_UTILITY_WRONG_TYPE, "wrong dialog type"); + } + return hleLogVerbose(Log::sceUtility, htmlViewerDialog->GetStatus()); } static u32 sceUtilityLoadUsbModule(u32 module) { @@ -1741,9 +1774,9 @@ const HLEFunction sceUtility[] = { {0X88BC7406, nullptr, "sceUtilitySavedataErrGetStatus", '?', "" }, {0XBDA7D894, &WrapI_V, "sceUtilityHtmlViewerGetStatus", 'i', "" }, - {0XCDC3AA41, nullptr, "sceUtilityHtmlViewerInitStart", '?', "" }, - {0XF5CE1134, nullptr, "sceUtilityHtmlViewerShutdownStart", '?', "" }, - {0X05AFB9E4, nullptr, "sceUtilityHtmlViewerUpdate", '?', "" }, + {0XCDC3AA41, &WrapI_U, "sceUtilityHtmlViewerInitStart", 'i', "x" }, + {0XF5CE1134, &WrapI_V, "sceUtilityHtmlViewerShutdownStart", 'i', "" }, + {0X05AFB9E4, &WrapI_I, "sceUtilityHtmlViewerUpdate", 'i', "i" }, {0X16A1A8D8, nullptr, "sceUtilityAuthDialogGetStatus", '?', "" }, {0X943CBA46, nullptr, "sceUtilityAuthDialogInitStart", '?', "" }, diff --git a/Core/HLE/sceUtility.h b/Core/HLE/sceUtility.h index 7a3b0e8c65..2240708796 100644 --- a/Core/HLE/sceUtility.h +++ b/Core/HLE/sceUtility.h @@ -117,6 +117,7 @@ enum class UtilityDialogType { GAMESHARING, GAMEDATAINSTALL, NPSIGNIN, + HTMLVIEWER, }; void __UtilityInit(); diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index b3a89ff63d..0cea4a1273 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -962,8 +962,11 @@ void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) { ShellExecute(NULL, L"open", wurl.c_str(), NULL, NULL, SW_SHOWNORMAL); #elif defined(__APPLE__) OSXOpenURL(std::string(url).c_str()); +#elif PPSSPP_PLATFORM(LINUX) + // Not through a shell: the URL can come from the game. + LaunchXdgOpen(std::string(url)); #else - std::string command = join("xdg-open ", url); + std::string command = "xdg-open '" + ReplaceAll(url, "'", "'\\''") + "'"; int err = system(command.c_str()); if (err) { INFO_LOG(Log::System, "Would have gone to %.*s but xdg-utils seems not to be installed", STR_VIEW(url)); @@ -1176,6 +1179,12 @@ bool System_GetPropertyBool(SystemProperty prop) { return true; #else return false; +#endif + case SYSPROP_CAN_LAUNCH_URL: +#if defined(MOBILE_DEVICE) && !PPSSPP_PLATFORM(SWITCH) + return false; +#else + return true; #endif case SYSPROP_HAS_OPEN_DIRECTORY: #if PPSSPP_PLATFORM(WINDOWS) diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index d48a75adc2..080025e76f 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -129,6 +129,7 @@ + @@ -416,6 +417,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 31536e4ae4..3d445e6e01 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -42,6 +42,7 @@ + @@ -472,6 +473,7 @@ + diff --git a/UWP/PPSSPP_UWPMain.cpp b/UWP/PPSSPP_UWPMain.cpp index 7f585c1ea5..2b64b1f191 100644 --- a/UWP/PPSSPP_UWPMain.cpp +++ b/UWP/PPSSPP_UWPMain.cpp @@ -463,6 +463,8 @@ bool System_GetPropertyBool(SystemProperty prop) { } case SYSPROP_HAS_FILE_BROWSER: return true; + case SYSPROP_CAN_LAUNCH_URL: + return true; case SYSPROP_HAS_FOLDER_BROWSER: return true; case SYSPROP_HAS_IMAGE_BROWSER: diff --git a/Windows/main.cpp b/Windows/main.cpp index 1de6919fd8..cca22423a8 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -450,6 +450,7 @@ bool System_GetPropertyBool(SystemProperty prop) { case SYSPROP_HAS_TEXT_INPUT_DIALOG: case SYSPROP_CAN_CREATE_SHORTCUT: case SYSPROP_CAN_SHOW_FILE: + case SYSPROP_CAN_LAUNCH_URL: case SYSPROP_HAS_TRASH_BIN: return true; case SYSPROP_HAS_IMAGE_BROWSER: diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 7f6610d2fe..f13ee4a087 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -688,6 +688,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/Dialog/PSPMsgDialog.cpp \ $(SRC)/Core/Dialog/PSPNetconfDialog.cpp \ $(SRC)/Core/Dialog/PSPNpSigninDialog.cpp \ + $(SRC)/Core/Dialog/PSPHtmlViewerDialog.cpp \ $(SRC)/Core/Dialog/PSPOskDialog.cpp \ $(SRC)/Core/Dialog/PSPOskConstants.cpp \ $(SRC)/Core/Dialog/PSPScreenshotDialog.cpp \ diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 6ec11565c2..69c4af8588 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -380,6 +380,8 @@ bool System_GetPropertyBool(SystemProperty prop) { return androidVersion >= 11; // honeycomb case SYSPROP_HAS_TEXT_CLIPBOARD: return true; + case SYSPROP_CAN_LAUNCH_URL: + return true; case SYSPROP_HAS_OPEN_DIRECTORY: return false; // We have this implemented but it may or may not work depending on if a file explorer is installed. case SYSPROP_HAS_ADDITIONAL_STORAGE: diff --git a/ios/main.mm b/ios/main.mm index ccbcee2576..e720fc0782 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -360,6 +360,8 @@ bool System_GetPropertyBool(SystemProperty prop) { switch (prop) { case SYSPROP_HAS_DEEP_LINKS: return true; + case SYSPROP_CAN_LAUNCH_URL: + return true; case SYSPROP_DISPLAY_HAS_CAMERA_CUTOUT: return true; case SYSPROP_HAS_FILE_BROWSER: diff --git a/libretro/Makefile.common b/libretro/Makefile.common index 264d7050a1..ad4b061e70 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -733,6 +733,7 @@ SOURCES_CXX += \ $(COREDIR)/Dialog/PSPMsgDialog.cpp \ $(COREDIR)/Dialog/PSPNetconfDialog.cpp \ $(COREDIR)/Dialog/PSPNpSigninDialog.cpp \ + $(COREDIR)/Dialog/PSPHtmlViewerDialog.cpp \ $(COREDIR)/Dialog/PSPOskDialog.cpp \ $(COREDIR)/Dialog/PSPOskConstants.cpp \ $(COREDIR)/Dialog/PSPPlaceholderDialog.cpp \ diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp index a217af86f6..e871544165 100644 --- a/libretro/libretro.cpp +++ b/libretro/libretro.cpp @@ -1991,6 +1991,7 @@ void System_RunOnMainThread(std::function) {} void NativeFrame(GraphicsContext *graphicsContext) {} void NativeResized() {} void System_Toast(std::string_view str) {} +void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) {} inline int16_t Clamp16(int32_t sample) { if (sample < -32767) return -32767; diff --git a/test.py b/test.py index 0f4915e38e..f3e7bca1d2 100755 --- a/test.py +++ b/test.py @@ -425,6 +425,7 @@ tests_good = [ "threads/vtimers/start", "threads/vtimers/stop", "threads/wakeup/wakeup", + "utility/dialog/htmlviewer", "utility/dialog/priority", "utility/dialog/status", "utility/msgdialog/abort",