Use SDL_OpenURL to open links and files on Android

Opening links and files with the `open_link` and `open_file` functions does not work on Android, as the `open_link` function uses `fork` which is not supported on Android. This also seems to cause a strange bug where client networking partially breaks. Currently, after trying to open any link, connecting to servers is not possible anymore but the server browser still works, with the connection getting stuck randomly in the connecting/loading state.

SDL implements URL opening, including of file URIs, with the `SDL_OpenURL` function for most systems including Android. However, using `SDL_OpenURL` for all systems has several downsides:

1. The `SDL_OpenURL` function is only available since SDL 2.0.14, in particular not for the Ubuntu 20 CI runner. Hence, we would either have to conditionally compile the link opening function to a null-implementation or fallback to using the existing `open_link` function.
2. We would be undoing some additional fixes in the `open_link` function for Windows, which are not included in the Windows implementation of `SDL_OpenURL`.
3. This would also replace the use of `open` on UNIX with `xdg-open`.
4. This would move the functionality to open links and files from the base to the engine client, so we could not have tools or the server potentially making use of this functionality in the future (e.g. open a folder for convenience).

Implementing link and file opening for Android ourselves is too much effort and potentially made even harder by SDL already managing all the unique JVM resources in the `SDLActivity`.

Therefore, the `SDL_OpenURL` function is only used for Android, which is always based on the latest SDL2 version. The original `open_link` functionality is kept for the other systems. For this purpose, the `IClient::ViewLink` and `ViewFile` functions are added to wrap `open_link` and `open_file` for the client and also reduce some duplicate code for error logging.

Unfortunately, testing also revealed that `SDL_OpenURL` does not currently support opening file URIs, at least not of files the internal storage location, which all the DDNet client's files would be located in. At least opening URLs works and neither breaks networking anymore.
This commit is contained in:
Robert Müller 2024-06-30 13:45:57 +02:00
parent 447b44d290
commit b05ca91a15
12 changed files with 94 additions and 61 deletions

View file

@ -4175,6 +4175,7 @@ bool is_process_alive(PROCESS process)
#endif
}
#if !defined(CONF_PLATFORM_ANDROID)
int open_link(const char *link)
{
#if defined(CONF_FAMILY_WINDOWS)
@ -4236,6 +4237,7 @@ int open_file(const char *path)
return open_link(buf);
#endif
}
#endif // !defined(CONF_PLATFORM_ANDROID)
struct SECURE_RANDOM_DATA
{

View file

@ -2583,6 +2583,7 @@ int kill_process(PROCESS process);
*/
bool is_process_alive(PROCESS process);
#if !defined(CONF_PLATFORM_ANDROID)
/**
* Opens a link in the browser.
*
@ -2598,11 +2599,11 @@ bool is_process_alive(PROCESS process);
int open_link(const char *link);
/**
* Opens a file or directory with default program.
* Opens a file or directory with the default program.
*
* @ingroup Shell
*
* @param path The path to open.
* @param path The file or folder to open with the default program.
*
* @return `1` on success, `0` on failure.
*
@ -2610,6 +2611,7 @@ int open_link(const char *link);
* @remark This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows.
*/
int open_file(const char *path);
#endif // !defined(CONF_PLATFORM_ANDROID)
/**
* @defgroup Secure-Random

View file

@ -289,6 +289,27 @@ public:
virtual CChecksumData *ChecksumData() = 0;
virtual int UdpConnectivity(int NetType) = 0;
/**
* Opens a link in the browser.
*
* @param pLink The link to open in a browser.
*
* @return `true` on success, `false` on failure.
*
* @remark This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows.
*/
virtual bool ViewLink(const char *pLink) = 0;
/**
* Opens a file or directory with the default program.
*
* @param pFilename The file or folder to open with the default program.
*
* @return `true` on success, `false` on failure.
*
* @remark This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows.
*/
virtual bool ViewFile(const char *pFilename) = 0;
#if defined(CONF_FAMILY_WINDOWS)
virtual void ShellRegister() = 0;
virtual void ShellUnregister() = 0;

View file

@ -4781,6 +4781,51 @@ int CClient::UdpConnectivity(int NetType)
return Connectivity;
}
bool CClient::ViewLink(const char *pLink)
{
#if defined(CONF_PLATFORM_ANDROID)
if(SDL_OpenURL(pLink) == 0)
{
return true;
}
log_error("client", "Failed to open link '%s' (%s)", pLink, SDL_GetError());
return false;
#else
if(open_link(pLink))
{
return true;
}
log_error("client", "Failed to open link '%s'", pLink);
return false;
#endif
}
bool CClient::ViewFile(const char *pFilename)
{
#if defined(CONF_PLATFORM_MACOS)
return ViewLink(pFilename);
#else
// Create a file link so the path can contain forward and
// backward slashes. But the file link must be absolute.
char aWorkingDir[IO_MAX_PATH_LENGTH];
if(fs_is_relative_path(pFilename))
{
if(!fs_getcwd(aWorkingDir, sizeof(aWorkingDir)))
{
log_error("client", "Failed to open file '%s' (failed to get working directory)", pFilename);
return false;
}
str_append(aWorkingDir, "/");
}
else
aWorkingDir[0] = '\0';
char aFileLink[IO_MAX_PATH_LENGTH];
str_format(aFileLink, sizeof(aFileLink), "file://%s%s", aWorkingDir, pFilename);
return ViewLink(aFileLink);
#endif
}
#if defined(CONF_FAMILY_WINDOWS)
void CClient::ShellRegister()
{

View file

@ -503,6 +503,9 @@ public:
CChecksumData *ChecksumData() override { return &m_Checksum.m_Data; }
int UdpConnectivity(int NetType) override;
bool ViewLink(const char *pLink) override;
bool ViewFile(const char *pFilename) override;
#if defined(CONF_FAMILY_WINDOWS)
void ShellRegister() override;
void ShellUnregister() override;

View file

@ -1625,10 +1625,7 @@ void CMenus::RenderPopupFullscreen(CUIRect Screen)
static CButtonContainer s_ButtonOpenFolder;
if(DoButton_Menu(&s_ButtonOpenFolder, Localize("Videos directory"), 0, &OpenFolder))
{
if(!open_file(aSaveFolder))
{
dbg_msg("menus", "couldn't open file '%s'", aSaveFolder);
}
Client()->ViewFile(aSaveFolder);
}
static CButtonContainer s_ButtonOk;

View file

@ -1458,10 +1458,7 @@ void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemAc
{
char aBuf[IO_MAX_PATH_LENGTH];
Storage()->GetCompletePath(m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, m_aCurrentDemoFolder[0] == '\0' ? "demos" : m_aCurrentDemoFolder, aBuf, sizeof(aBuf));
if(!open_file(aBuf))
{
dbg_msg("menus", "couldn't open file '%s'", aBuf);
}
Client()->ViewFile(aBuf);
}
GameClient()->m_Tooltips.DoToolTip(&s_DemosDirectoryButton, &DemosDirectoryButton, Localize("Open the directory that contains the demo files"));
}

View file

@ -1166,10 +1166,7 @@ void CMenus::RenderGhost(CUIRect MainView)
char aBuf[IO_MAX_PATH_LENGTH];
Storage()->GetCompletePath(IStorage::TYPE_SAVE, "ghosts", aBuf, sizeof(aBuf));
Storage()->CreateFolder("ghosts", IStorage::TYPE_SAVE);
if(!open_file(aBuf))
{
dbg_msg("menus", "couldn't open file '%s'", aBuf);
}
Client()->ViewFile(aBuf);
}
Status.VSplitLeft(5.0f, &Button, &Status);

View file

@ -77,8 +77,8 @@ bool CMenusKeyBinder::OnInput(const IInput::CEvent &Event)
void CMenus::RenderSettingsGeneral(CUIRect MainView)
{
char aBuf[128 + IO_MAX_PATH_LENGTH];
CUIRect Label, Button, Left, Right, Game, Client;
MainView.HSplitTop(150.0f, &Game, &Client);
CUIRect Label, Button, Left, Right, Game, ClientSettings;
MainView.HSplitTop(150.0f, &Game, &ClientSettings);
// game
{
@ -139,10 +139,10 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView)
// client
{
// headline
Client.HSplitTop(30.0f, &Label, &Client);
ClientSettings.HSplitTop(30.0f, &Label, &ClientSettings);
Ui()->DoLabel(&Label, Localize("Client"), 20.0f, TEXTALIGN_ML);
Client.HSplitTop(5.0f, nullptr, &Client);
Client.VSplitMid(&Left, &Right, 20.0f);
ClientSettings.HSplitTop(5.0f, nullptr, &ClientSettings);
ClientSettings.VSplitMid(&Left, &Right, 20.0f);
// skip main menu
Left.HSplitTop(20.0f, &Button, &Left);
@ -165,10 +165,7 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView)
if(DoButton_Menu(&s_SettingsButtonId, Localize("Settings file"), 0, &SettingsButton))
{
Storage()->GetCompletePath(IStorage::TYPE_SAVE, CONFIG_FILE, aBuf, sizeof(aBuf));
if(!open_file(aBuf))
{
dbg_msg("menus", "couldn't open file '%s'", aBuf);
}
Client()->ViewFile(aBuf);
}
GameClient()->m_Tooltips.DoToolTip(&s_SettingsButtonId, &SettingsButton, Localize("Open the settings file"));
@ -179,10 +176,7 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView)
if(DoButton_Menu(&s_ConfigButtonId, Localize("Config directory"), 0, &ConfigButton))
{
Storage()->GetCompletePath(IStorage::TYPE_SAVE, "", aBuf, sizeof(aBuf));
if(!open_file(aBuf))
{
dbg_msg("menus", "couldn't open file '%s'", aBuf);
}
Client()->ViewFile(aBuf);
}
GameClient()->m_Tooltips.DoToolTip(&s_ConfigButtonId, &ConfigButton, Localize("Open the directory that contains the configuration and user files"));
@ -194,10 +188,7 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView)
{
Storage()->GetCompletePath(IStorage::TYPE_SAVE, "themes", aBuf, sizeof(aBuf));
Storage()->CreateFolder("themes", IStorage::TYPE_SAVE);
if(!open_file(aBuf))
{
dbg_msg("menus", "couldn't open file '%s'", aBuf);
}
Client()->ViewFile(aBuf);
}
GameClient()->m_Tooltips.DoToolTip(&s_ThemesButtonId, &DirectoryButton, Localize("Open the directory to add custom themes"));
@ -938,11 +929,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
static CButtonContainer s_SkinDatabaseButton;
if(DoButton_Menu(&s_SkinDatabaseButton, Localize("Skin Database"), 0, &DatabaseButton))
{
const char *pLink = "https://ddnet.org/skins/";
if(!open_link(pLink))
{
dbg_msg("menus", "couldn't open link '%s'", pLink);
}
Client()->ViewLink("https://ddnet.org/skins/");
}
static CButtonContainer s_DirectoryButton;
@ -950,10 +937,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView)
{
Storage()->GetCompletePath(IStorage::TYPE_SAVE, "skins", aBuf, sizeof(aBuf));
Storage()->CreateFolder("skins", IStorage::TYPE_SAVE);
if(!open_file(aBuf))
{
dbg_msg("menus", "couldn't open file '%s'", aBuf);
}
Client()->ViewFile(aBuf);
}
GameClient()->m_Tooltips.DoToolTip(&s_DirectoryButton, &DirectoryButton, Localize("Open the directory to add custom skins"));

View file

@ -648,10 +648,7 @@ void CMenus::RenderSettingsCustom(CUIRect MainView)
Storage()->GetCompletePath(IStorage::TYPE_SAVE, aBufFull, aBuf, sizeof(aBuf));
Storage()->CreateFolder("assets", IStorage::TYPE_SAVE);
Storage()->CreateFolder(aBufFull, IStorage::TYPE_SAVE);
if(!open_file(aBuf))
{
dbg_msg("menus", "couldn't open file '%s'", aBuf);
}
Client()->ViewFile(aBuf);
}
GameClient()->m_Tooltips.DoToolTip(&s_AssetsDirId, &DirectoryButton, Localize("Open the directory to add custom assets"));

View file

@ -43,11 +43,7 @@ void CMenus::RenderStartMenu(CUIRect MainView)
static CButtonContainer s_DiscordButton;
if(DoButton_Menu(&s_DiscordButton, Localize("Discord"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)))
{
const char *pLink = Localize("https://ddnet.org/discord");
if(!open_link(pLink))
{
dbg_msg("menus", "couldn't open link '%s'", pLink);
}
Client()->ViewLink(Localize("https://ddnet.org/discord"));
}
ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space
@ -55,11 +51,7 @@ void CMenus::RenderStartMenu(CUIRect MainView)
static CButtonContainer s_LearnButton;
if(DoButton_Menu(&s_LearnButton, Localize("Learn"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)))
{
const char *pLink = Localize("https://wiki.ddnet.org/");
if(!open_link(pLink))
{
dbg_msg("menus", "couldn't open link '%s'", pLink);
}
Client()->ViewLink(Localize("https://wiki.ddnet.org/"));
}
ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space
@ -96,11 +88,7 @@ void CMenus::RenderStartMenu(CUIRect MainView)
static CButtonContainer s_WebsiteButton;
if(DoButton_Menu(&s_WebsiteButton, Localize("Website"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)))
{
const char *pLink = "https://ddnet.org/";
if(!open_link(pLink))
{
dbg_msg("menus", "couldn't open link '%s'", pLink);
}
Client()->ViewLink("https://ddnet.org/");
}
ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space

View file

@ -5396,7 +5396,7 @@ void CEditor::RenderFileDialog()
{
char aOpenPath[IO_MAX_PATH_LENGTH];
Storage()->GetCompletePath(m_FilesSelectedIndex >= 0 ? m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, m_pFileDialogPath, aOpenPath, sizeof(aOpenPath));
if(!open_file(aOpenPath))
if(!Client()->ViewFile(aOpenPath))
{
ShowFileDialogError("Failed to open the directory '%s'.", aOpenPath);
}
@ -7712,7 +7712,7 @@ void CEditor::RenderMenubar(CUIRect MenuBar)
if(DoButton_Editor(&s_HelpButton, "?", 0, &Help, 0, "[F1] Open the DDNet Wiki page for the Map Editor in a web browser") || (Input()->KeyPress(KEY_F1) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr))
{
const char *pLink = Localize("https://wiki.ddnet.org/wiki/Mapping");
if(!open_link(pLink))
if(!Client()->ViewLink(pLink))
{
ShowFileDialogError("Failed to open the link '%s' in the default web browser.", pLink);
}