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",