diff --git a/Common/System/Request.h b/Common/System/Request.h index 80461f7756..d55a450e81 100644 --- a/Common/System/Request.h +++ b/Common/System/Request.h @@ -183,6 +183,19 @@ inline void System_ShareFile(const Path &path, std::string_view mimeType = "") { g_requestManager.MakeSystemRequest(SystemRequestType::SHARE_FILE, NO_REQUESTER_TOKEN, nullptr, nullptr, path.ToString(), mimeType, 0); } +// Talking to other devices on the local network (in either direction) needs a runtime permission on +// Android 17+. Asks for it unless it's granted or already being asked for. Returns true if granted. +inline bool System_RequestLocalNetworkPermission() { + PermissionStatus status = System_GetPermissionStatus(SYSTEM_PERMISSION_LOCAL_NETWORK); + if (status == PERMISSION_STATUS_GRANTED) { + return true; + } + if (status != PERMISSION_STATUS_PENDING) { + System_AskForPermission(SYSTEM_PERMISSION_LOCAL_NETWORK); + } + return false; +} + // TEXT_GOTFOCUS/TEXT_LOSTFOCUS (and POPUP_CLOSED) sent through here count as the regular UI's use of text input. void System_NotifyUIEvent(UIEventNotification notification); diff --git a/Core/HLE/sceNetAdhoc.cpp b/Core/HLE/sceNetAdhoc.cpp index 74e475c577..aeb8202d42 100644 --- a/Core/HLE/sceNetAdhoc.cpp +++ b/Core/HLE/sceNetAdhoc.cpp @@ -20,6 +20,8 @@ // All credit goes to him! +#include "ppsspp_config.h" + #include #include @@ -31,6 +33,7 @@ #include "Common/Serialize/SerializeFuncs.h" #include "Common/Serialize/SerializeMap.h" #include "Common/System/OSD.h" +#include "Common/System/Request.h" #include "Common/System/System.h" #include "Common/File/VFS/VFS.h" #include "Common/Thread/ThreadUtil.h" @@ -103,6 +106,21 @@ int gameModeNotifyEvent = -1; #define AEMU_POSTOFFICE_ID_BASE 2048 #define RELAY_FAILURE_DISABLE_THRESHOLD_SEC 15 +// Without the local network permission, Android 17+ fails sends to LAN peers with EPERM. +static void WarnIfLocalNetworkBlocked(int error) { +#if PPSSPP_PLATFORM(ANDROID) + static double lastWarning = -100.0; + if (error != EPERM || time_now_d() - lastWarning < 10.0) { + return; + } + if (System_GetPermissionStatus(SYSTEM_PERMISSION_LOCAL_NETWORK) != PERMISSION_STATUS_GRANTED) { + lastWarning = time_now_d(); + auto n = GetI18NCategory(I18NCat::NETWORKING); + g_OSD.Show(OSDType::MESSAGE_ERROR, n->T("Local network permission is needed to play with nearby players"), 5.0f, "local_network_blocked"); + } +#endif +} + static const char *AdhocDataModeToString(AdhocDataMode mode) { switch (mode) { case AdhocDataMode::P2P: @@ -1994,6 +2012,11 @@ int sceNetAdhocctlInit(int stackSize, int prio, u32 productAddr) { return hleLogError(Log::sceNet, SCE_NET_ADHOCCTL_ERROR_ALREADY_INITIALIZED); } + if (g_Config.bEnableWlan) { + // Needed for peers and ad hoc servers on the same LAN (Android 17+). + System_RequestLocalNetworkPermission(); + } + auto product = PSPPointer::Create(productAddr); if (product.IsValid()) { product_code = *product; @@ -2403,6 +2426,7 @@ int sceNetAdhocPdpSend(int id, const char *mac, u32 port, void *data, int len, i } DEBUG_LOG(Log::sceNet, "Socket Error (%i) on sceNetAdhocPdpSend[%i:%u->%u] (size=%i)", error, id, getLocalPort(pdpsocket.id), ntohs(target.sin_port), len); + WarnIfLocalNetworkBlocked(error); } //changeBlockingMode(socket->id, 0); @@ -2502,6 +2526,7 @@ int sceNetAdhocPdpSend(int id, const char *mac, u32 port, void *data, int len, i if (sent == SOCKET_ERROR) { DEBUG_LOG(Log::sceNet, "Socket Error (%i) on sceNetAdhocPdpSend[%i:%u->%u](BC) [size=%i]", error, id, getLocalPort(pdpsocket.id), ntohs(target.sin_port), len); + WarnIfLocalNetworkBlocked(error); } if (sent >= 0) { diff --git a/UI/DeveloperToolsScreen.cpp b/UI/DeveloperToolsScreen.cpp index 2b97b69d19..53efdc1793 100644 --- a/UI/DeveloperToolsScreen.cpp +++ b/UI/DeveloperToolsScreen.cpp @@ -21,6 +21,7 @@ #include "Common/UI/View.h" #include "Common/UI/ViewGroup.h" #include "Common/System/OSD.h" +#include "Common/System/Request.h" #include "Common/GPU/OpenGL/GLFeatures.h" #include "Common/Data/Text/Parsers.h" #include "Common/Data/Encoding/Utf8.h" @@ -736,6 +737,8 @@ void DeveloperToolsScreen::OnCopyStatesToRoot(UI::EventParams &e) { void DeveloperToolsScreen::OnRemoteDebugger(UI::EventParams &e) { if (allowDebugger_) { + // Only needed for connecting from another device, but that's the usual case. + System_RequestLocalNetworkPermission(); StartWebServer(WebServerFlags::DEBUGGER); } else { StopWebServer(WebServerFlags::DEBUGGER); diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 2a7ec8b1f2..e1a8d565a5 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -890,6 +890,9 @@ void EmuScreen::ProcessVKey(VirtKey virtKey, bool down) { auto n = GetI18NCategory(I18NCat::NETWORKING); auto di = GetI18NCategory(I18NCat::DIALOG); g_Config.bEnableWlan = !g_Config.bEnableWlan; + if (g_Config.bEnableWlan) { + System_RequestLocalNetworkPermission(); + } // Try to avoid adding more strings so we piece together a message from existing ones. g_OSD.Show(OSDType::MESSAGE_INFO, StringFromFormat( "%s: %s", n->T_cstr("Enable networking"), g_Config.bEnableWlan ? di->T_cstr("Enabled") : di->T_cstr("Disabled")), 2.0, "toggle_wlan"); diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index bebfca473a..4df70181c1 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -979,7 +979,15 @@ void GameSettingsScreen::CreateNetworkingSettings(UI::ViewGroup *networkingSetti System_LaunchUrl(LaunchUrlType::BROWSER_URL, url); }); - networkingSettings->Add(new CheckBox(&g_Config.bEnableWlan, n->T("Enable networking", "Enable networking/wlan (beta)"))); + // Ad hoc play, the built-in server and UPnP all talk to the local network. + auto requestLocalNetwork = [](UI::EventParams &e) { + CheckBox *checkBox = static_cast(e.v); + if (checkBox->Toggled()) { + System_RequestLocalNetworkPermission(); + } + }; + + networkingSettings->Add(new CheckBox(&g_Config.bEnableWlan, n->T("Enable networking", "Enable networking/wlan (beta)")))->OnClick.Add(requestLocalNetwork); networkingSettings->Add(new MacAddressChooser(GetRequesterToken(), gamePath_, &g_Config.sMACAddress, n->T("MAC address"), screenManager())); networkingSettings->Add(new ItemHeader(n->T("Ad Hoc multiplayer"))); @@ -1027,7 +1035,7 @@ void GameSettingsScreen::CreateNetworkingSettings(UI::ViewGroup *networkingSetti }); networkingSettings->Add(new SettingHint(n->T("Try to pick a unique nickname for ad hoc play"), nullptr)); - networkingSettings->Add(new CheckBox(&g_Config.bEnableAdhocServer, n->T("Enable built-in ad hoc server"))); + networkingSettings->Add(new CheckBox(&g_Config.bEnableAdhocServer, n->T("Enable built-in ad hoc server")))->OnClick.Add(requestLocalNetwork); networkingSettings->Add(new ItemHeader(n->T("Infrastructure"))); if (g_Config.sInfrastructureUsername.empty()) { @@ -1048,7 +1056,8 @@ void GameSettingsScreen::CreateNetworkingSettings(UI::ViewGroup *networkingSetti // and a game that's already mapped its ports wouldn't cope with them disappearing. CheckBox *enableUPnP = networkingSettings->Add(new CheckBox(&g_Config.bEnableUPnP, n->T("Enable UPnP", "Enable UPnP (need a few seconds to detect)"))); enableUPnP->SetEnabled(!PSP_IsInited()); - enableUPnP->OnClick.Add([](UI::EventParams &e) { + enableUPnP->OnClick.Add([requestLocalNetwork](UI::EventParams &e) { + requestLocalNetwork(e); // Wake the UPnP service thread so it connects (or tears its mappings back down) right // away, instead of waiting for a port request that may never come. UPnP_Notify(); diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 1341b5fb36..806051b3fb 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -845,6 +845,8 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO flags |= WebServerFlags::DEBUGGER; } if (flags != WebServerFlags::NONE) { + // Both are for other devices to connect to. + System_RequestLocalNetworkPermission(); StartWebServer(flags); } diff --git a/UI/RemoteISOScreen.cpp b/UI/RemoteISOScreen.cpp index 3ba4d47d44..0917204f39 100644 --- a/UI/RemoteISOScreen.cpp +++ b/UI/RemoteISOScreen.cpp @@ -383,7 +383,11 @@ void RemoteISOScreen::CreateSettingsTab(UI::ViewGroup *remoteisoSettings) { auto ri = GetI18NCategory(I18NCat::REMOTEISO); remoteisoSettings->Add(new ItemHeader(ri->T("Remote disc streaming"))); - remoteisoSettings->Add(new CheckBox(&g_Config.bRemoteShareOnStartup, ri->T("Share on PPSSPP startup"))); + remoteisoSettings->Add(new CheckBox(&g_Config.bRemoteShareOnStartup, ri->T("Share on PPSSPP startup")))->OnClick.Add([](UI::EventParams &e) { + if (g_Config.bRemoteShareOnStartup) { + System_RequestLocalNetworkPermission(); + } + }); remoteisoSettings->Add(new CheckBox(&g_Config.bRemoteISOManual, ri->T("Manual Mode Client", "Manually configure client"))); remoteisoSettings->Add(new CheckBox(&g_Config.bRemoteTab, ri->T("Show Remote tab on main screen"))); diff --git a/UI/UploadScreen.cpp b/UI/UploadScreen.cpp index 1c8941b5a6..8bf56ec632 100644 --- a/UI/UploadScreen.cpp +++ b/UI/UploadScreen.cpp @@ -6,7 +6,6 @@ #include "Common/Data/Text/Parsers.h" #include "Common/Data/Text/I18n.h" #include "Common/UI/Notice.h" -#include "Common/System/System.h" #include "Core/WebServer.h" #include "Core/Util/PathUtil.h" #include "UI/UploadScreen.h" @@ -21,11 +20,8 @@ UploadScreen::UploadScreen(const Path &targetFolder) : UISimpleBaseDialogScreen( localIPs_.push_back(ip); } } - // On Android 17+, serving to other devices on the local network needs a runtime permission. + System_RequestLocalNetworkPermission(); prevPermission_ = System_GetPermissionStatus(SYSTEM_PERMISSION_LOCAL_NETWORK); - if (prevPermission_ != PERMISSION_STATUS_GRANTED) { - System_AskForPermission(SYSTEM_PERMISSION_LOCAL_NETWORK); - } StartServerIfPermitted(); } diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index f8ed3ffad4..f4b4ffca05 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -1021,6 +1021,7 @@ Infrastructure = Infrastructure Infrastructure server provided by: = Infrastructure server provided by: Invalid IP or hostname = Invalid IP or hostname Local network addresses = Local network addresses +Local network permission is needed to play with nearby players = Local network permission is needed to play with nearby players Local network permission is needed to receive files = Local network permission is needed to receive files MAC address = MAC address Minimum Timeout = Minimum timeout (override in ms, 0 = default)