diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 9d9ab2788..93bd9fa23 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -123,7 +123,9 @@ if(IOS) elseif(APPLE) include_directories(${COMMON_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/src) add_executable(ES-DE ${ES_SOURCES} ${ES_HEADERS}) - target_link_libraries(ES-DE ${COMMON_LIBRARIES} es-core) + target_link_libraries(ES-DE ${COMMON_LIBRARIES} "-framework CoreFoundation -framework IOKit" + "-framework SystemConfiguration" + "-framework IOBluetooth" es-core) set_target_properties(ES-DE PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE) if(CMAKE_CXX_COMPILER_VERSION GREATER_EQUAL 15.0.0) target_link_options(ES-DE PRIVATE LINKER:-no_warn_duplicate_libraries) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 88ac66d7e..9fe8ce2a4 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -26,6 +26,7 @@ #include "Settings.h" #include "Sound.h" #include "SystemData.h" +#include "SystemStatus.h" #include "guis/GuiDetectDevice.h" #include "guis/GuiLaunchScreen.h" #include "utils/FileSystemUtil.h" @@ -1165,6 +1166,7 @@ int main(int argc, char* argv[]) } #endif + SystemStatus::getInstance(); MameNames::getInstance(); ThemeData::populateThemes(); loadSystemsReturnCode loadSystemsStatus {loadSystemConfigFile()}; diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index 15510c3ac..c8908506a 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -22,6 +22,7 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/MameNames.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Settings.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemStatus.h ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Window.h @@ -110,6 +111,7 @@ set(CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Settings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemStatus.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Window.cpp @@ -179,6 +181,10 @@ if(ANDROID) set(CORE_SOURCES ${CORE_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/PlatformUtilAndroid.cpp) endif() +if(APPLE AND NOT IOS) + set(CORE_HEADERS ${CORE_HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/src/BluetoothStatusApple.m) +endif() + if(IOS) set(CORE_HEADERS ${CORE_HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/src/InputOverlay.h) set(CORE_HEADERS ${CORE_HEADERS} ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/PlatformUtilIOS.h) diff --git a/es-core/src/BluetoothStatusApple.h b/es-core/src/BluetoothStatusApple.h new file mode 100644 index 000000000..069c42857 --- /dev/null +++ b/es-core/src/BluetoothStatusApple.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// +// ES-DE Frontend +// BluetoothStatusApple.h +// +// Gets the Bluetooth adapter status on macOS. +// + +#ifndef ES_CORE_BLUETOOTH_STATUS_APPLE_H +#define ES_CORE_BLUETOOTH_STATUS_APPLE_H + +#ifdef __cplusplus +extern "C" { +int getBluetoothStatus(); +} +#endif + +#endif // ES_CORE_BLUETOOTH_STATUS_APPLE_H diff --git a/es-core/src/BluetoothStatusApple.m b/es-core/src/BluetoothStatusApple.m new file mode 100644 index 000000000..228f062ba --- /dev/null +++ b/es-core/src/BluetoothStatusApple.m @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +// +// ES-DE Frontend +// BluetoothStatusApple.m +// +// Gets the Bluetooth adapter status on macOS. +// + +#import "BluetoothStatusApple.h" + +#import + +int getBluetoothStatus() +{ + IOBluetoothHostController* hciController = [IOBluetoothHostController defaultController]; + + if (hciController != NULL && hciController.powerState) + return 1; + else + return 0; +} diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index f167b9d21..e65da2695 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -221,6 +221,12 @@ void Settings::setDefaults() #endif mBoolMap["ScreensaverVideoBlur"] = {false, false}; + mBoolMap["SystemStatusDisplayAll"] = {false, false}; + mBoolMap["SystemStatusBluetooth"] = {true, true}; + mBoolMap["SystemStatusWifi"] = {true, true}; + mBoolMap["SystemStatusCellular"] = {true, true}; + mBoolMap["SystemStatusBattery"] = {true, true}; + mBoolMap["SystemStatusBatteryPercentage"] = {true, true}; mBoolMap["ThemeVariantTriggers"] = {true, true}; mBoolMap["DisplayClock"] = {false, false}; mBoolMap["MenuBlurBackground"] = {true, true}; diff --git a/es-core/src/SystemStatus.cpp b/es-core/src/SystemStatus.cpp new file mode 100644 index 000000000..3fc15edeb --- /dev/null +++ b/es-core/src/SystemStatus.cpp @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: MIT +// +// ES-DE Frontend +// SystemStatus.cpp +// +// Queries system status information from the operating system. +// This includes Bluetooth, Wi-Fi, cellular and battery. +// + +#include "SystemStatus.h" + +#include "Log.h" +#include "Settings.h" +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" + +#include + +#include + +#if defined(__APPLE__) && !defined(__IOS__) +#include "BluetoothStatusApple.h" +#include +#include +#include +#endif + +#if defined(_WIN64) +// clang-format off +// Because of course building fails if the files are included in the "wrong" order. +#include +#include +#include +// clang-format on +#endif + +#if defined(__ANDROID__) +#include "utils/PlatformUtilAndroid.h" +#endif + +#define DEBUG_SYSTEM_STATUS false + +SystemStatus::SystemStatus() noexcept + : mExitPolling {false} + , mPollImmediately {false} + , mHasBluetooth {false} + , mHasWifi {false} + , mHasCellular {false} + , mHasBattery {false} + , mBatteryCharging {false} + , mBatteryCapacity {0} + +{ + setCheckFlags(); + mPollThread = std::make_unique(&SystemStatus::pollStatus, this); +} + +SystemStatus::~SystemStatus() +{ + mExitPolling = true; + + if (mPollThread != nullptr && mPollThread->joinable()) { + mPollThread->join(); + mPollThread.reset(); + } +} + +SystemStatus& SystemStatus::getInstance() +{ + static SystemStatus instance; + return instance; +} + +void SystemStatus::setCheckFlags() +{ + std::unique_lock statusLock {mStatusMutex}; + mCheckBluetooth = Settings::getInstance()->getBool("SystemStatusBluetooth"); + mCheckWifi = Settings::getInstance()->getBool("SystemStatusWifi"); + mCheckCellular = Settings::getInstance()->getBool("SystemStatusCellular"); + mCheckBattery = Settings::getInstance()->getBool("SystemStatusBattery"); +} + +void SystemStatus::setPolling(const bool state) +{ + if (state == false) { + mExitPolling = true; + if (mPollThread != nullptr && mPollThread->joinable()) { + mPollThread->join(); + mPollThread.reset(); + } + } + else if (mPollThread == nullptr) { + mExitPolling = false; + mPollThread = std::make_unique(&SystemStatus::pollStatus, this); + } +} + +SystemStatus::Status SystemStatus::getStatus() +{ + mStatus.hasBluetooth = mHasBluetooth; + mStatus.hasWifi = mHasWifi; + mStatus.hasCellular = mHasCellular; + mStatus.hasBattery = mHasBattery; + mStatus.batteryCharging = mBatteryCharging; + mStatus.batteryCapacity = mBatteryCapacity; + + return mStatus; +} + +void SystemStatus::pollStatus() +{ + while (!mExitPolling) { + std::unique_lock statusLock {mStatusMutex}; + + getStatusBluetooth(); + getStatusWifi(); + getStatusCellular(); + getStatusBattery(); + statusLock.unlock(); + +#if (DEBUG_SYSTEM_STATUS) + std::string status {"Bluetooth "}; + status.append(mHasBluetooth ? "enabled" : "disabled") + .append(", Wi-Fi ") + .append(mHasBluetooth ? "enabled" : "disabled") + .append(", cellular ") + .append(mHasCellular ? "enabled" : "disabled") + .append(", battery ") + .append(mHasBattery ? "enabled" : "disabled"); + if (mHasBattery) { + status.append(" (") + .append(mBatteryCharging ? "charging" : "not charging") + .append(" and at ") + .append(std::to_string(mBatteryCapacity)) + .append("% capacity)"); + } + LOG(LogDebug) << "SystemStatus::pollStatus(): " << status; +#endif + + int delayValue {0}; + while (!mPollImmediately && !mExitPolling && delayValue < 3000) { + delayValue += 100; + SDL_Delay(100); + } + + mPollImmediately = false; + } +} + +void SystemStatus::getStatusBluetooth() +{ + if (!mCheckBluetooth) + return; + + bool hasBluetooth {false}; + +#if defined(__APPLE__) && !defined(__IOS__) + if (getBluetoothStatus() == 1) + hasBluetooth = true; + +#elif defined(_WIN64) + BLUETOOTH_FIND_RADIO_PARAMS btFindRadio {sizeof(BLUETOOTH_FIND_RADIO_PARAMS)}; + HANDLE btRadio {nullptr}; + BLUETOOTH_RADIO_INFO btInfo {sizeof(BLUETOOTH_RADIO_INFO), 0}; + + if (BluetoothFindFirstRadio(&btFindRadio, &btRadio) != nullptr) { + if (BluetoothGetRadioInfo(btRadio, &btInfo) == ERROR_SUCCESS) + hasBluetooth = true; + } + +#elif defined(__ANDROID__) + if (Utils::Platform::Android::getBluetoothStatus()) + hasBluetooth = true; + +#elif defined(__linux__) + const std::string sysEntry {"/sys/class/rfkill"}; + auto entries {Utils::FileSystem::getDirContent(sysEntry, false)}; + for (auto& entry : entries) { + if (Utils::FileSystem::exists(entry + "/type")) { + std::string type; + std::ifstream fileStream; + fileStream.open(entry + "/type"); + getline(fileStream, type); + fileStream.close(); + if (Utils::String::toLower(type) == "bluetooth") { + std::string state; + fileStream.open(entry + "/state"); + getline(fileStream, state); + fileStream.close(); + if (std::stoi(state) == 1) + hasBluetooth = true; + break; + } + } + } +#endif + + mHasBluetooth = hasBluetooth; +} + +void SystemStatus::getStatusWifi() +{ + if (!mCheckWifi) + return; + + bool hasWifi {false}; + +#if defined(__APPLE__) && !defined(__IOS__) + const CFArrayRef interfaces {SCNetworkInterfaceCopyAll()}; + + if (interfaces != nullptr) { + for (CFIndex i {0}; i < CFArrayGetCount(interfaces); ++i) { + SCNetworkInterfaceRef interface { + static_cast(CFArrayGetValueAtIndex(interfaces, i))}; + + if (SCNetworkInterfaceGetInterfaceType(interface) == kSCNetworkInterfaceTypeIEEE80211) { + const CFStringRef bsdName {SCNetworkInterfaceGetBSDName(interface)}; + + const SCDynamicStoreRef session { + SCDynamicStoreCreate(nullptr, CFSTR("Custom"), nullptr, nullptr)}; + + const CFStringRef resolvedQuery {CFStringCreateWithFormat( + nullptr, nullptr, CFSTR("State:/Network/Interface/%@/IPv4"), bsdName)}; + + const CFDictionaryRef dict { + static_cast(SCDynamicStoreCopyValue(session, resolvedQuery))}; + + if (dict != nullptr) { + hasWifi = true; + CFRelease(dict); + CFRelease(resolvedQuery); + CFRelease(session); + break; + } + else { + CFRelease(resolvedQuery); + CFRelease(session); + } + } + } + CFRelease(interfaces); + } + +#elif defined(_WIN64) + PIP_ADAPTER_INFO pAdapterInfo {nullptr}; + PIP_ADAPTER_INFO pAdapter {nullptr}; + ULONG ulOutBufLen {sizeof(IP_ADAPTER_INFO)}; + pAdapterInfo = reinterpret_cast(malloc(sizeof(IP_ADAPTER_INFO))); + + if (pAdapterInfo != nullptr) { + // Make an initial call to GetAdaptersInfo to get the necessary size into the + // ulOutBufLen variable, which may or may not be big enough. + if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) == ERROR_BUFFER_OVERFLOW) { + free(pAdapterInfo); + pAdapterInfo = reinterpret_cast(malloc(ulOutBufLen)); + } + if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) == NO_ERROR) { + pAdapter = pAdapterInfo; + while (pAdapter) { + if (pAdapter->Type == IF_TYPE_IEEE80211) { + // Checking whether the interface has an IP address is crude but + // it seems to get the job done. And there is no other obvious + // way to query the interface status without using additional + // convoluted API calls. + if (const std::string {pAdapter->IpAddressList.IpAddress.String} != "0.0.0.0") { + hasWifi = true; + break; + } + } + pAdapter = pAdapter->Next; + } + } + + if (pAdapterInfo) + free(pAdapterInfo); + } + +#elif defined(__ANDROID__) + if (Utils::Platform::Android::getWifiStatus() == 1) + hasWifi = true; + +#elif defined(__linux__) + const std::string sysEntry {"/sys/class/net"}; + auto entries {Utils::FileSystem::getDirContent(sysEntry, false)}; + for (auto& entry : entries) { + if (Utils::FileSystem::exists(entry + "/wireless") && + Utils::FileSystem::exists(entry + "/operstate")) { + std::string wifiState; + std::ifstream fileStream; + fileStream.open(entry + "/operstate"); + getline(fileStream, wifiState); + fileStream.close(); + if (Utils::String::toLower(wifiState) == "up") + hasWifi = true; + } + } +#endif + + mHasWifi = hasWifi; +} + +void SystemStatus::getStatusCellular() +{ + if (!mCheckCellular) + return; + + bool hasCellular {false}; + +#if defined(__ANDROID__) + if (Utils::Platform::Android::getCellularStatus() >= 1) + hasCellular = true; +#endif + + mHasCellular = hasCellular; +} + +void SystemStatus::getStatusBattery() +{ + if (!mCheckBattery) + return; + + bool hasBattery {false}; + bool batteryCharging {false}; + int batteryCapacity {0}; + +#if defined(__APPLE__) && !defined(__IOS__) + CFTypeRef sourceInfo {IOPSCopyPowerSourcesInfo()}; + CFArrayRef sourceList {IOPSCopyPowerSourcesList(sourceInfo)}; + + if (sourceList != nullptr && CFArrayGetCount(sourceList) > 0) { + CFDictionaryRef source {nullptr}; + + for (CFIndex i {0}; i < CFArrayGetCount(sourceList); ++i) { + source = + IOPSGetPowerSourceDescription(sourceInfo, CFArrayGetValueAtIndex(sourceList, i)); + // Check if this is a battery. + const CFStringRef type {static_cast( + CFDictionaryGetValue(source, CFSTR(kIOPSTransportTypeKey)))}; + if (kCFCompareEqualTo == CFStringCompare(type, CFSTR(kIOPSInternalType), 0)) + break; + else + source = nullptr; + } + + if (source != nullptr) { + hasBattery = true; + + if (CFDictionaryGetValue(source, CFSTR(kIOPSIsChargingKey)) != nullptr) { + batteryCharging = CFBooleanGetValue(static_cast( + CFDictionaryGetValue(source, CFSTR(kIOPSIsChargingKey)))); + } + + int curCapacity {0}; + const CFNumberRef curCapacityNum {static_cast( + CFDictionaryGetValue(source, CFSTR(kIOPSCurrentCapacityKey)))}; + CFNumberGetValue(curCapacityNum, kCFNumberIntType, &curCapacity); + + int maxCapacity {0}; + const CFNumberRef maxCapacityNum { + static_cast(CFDictionaryGetValue(source, CFSTR(kIOPSMaxCapacityKey)))}; + CFNumberGetValue(maxCapacityNum, kCFNumberIntType, &maxCapacity); + + if (maxCapacity > 0) + batteryCapacity = curCapacity / maxCapacity * 100; + } + } + + if (sourceInfo != nullptr) + CFRelease(sourceInfo); + if (sourceList != nullptr) + CFRelease(sourceList); + +#elif defined(_WIN64) + SYSTEM_POWER_STATUS powerStatus; + + if (GetSystemPowerStatus(&powerStatus)) { + if (powerStatus.BatteryFlag != 128 && powerStatus.BatteryFlag != 255) { + hasBattery = true; + + if (powerStatus.ACLineStatus == 1) + atteryCharging = true; + + batteryCapacity = powerStatus.BatteryLifePercent; + } + else { + hasBattery = false; + } + } + +#elif defined(__ANDROID__) + std::pair batteryStatus {Utils::Platform::Android::getBatteryStatus()}; + hasBattery = static_cast(batteryStatus.first); + + if (batteryStatus.first == -1 && batteryStatus.second == -1) { + hasBattery = false; + } + else { + hasBattery = true; + if (batteryStatus.first == 1) + batteryCharging = true; + } + + batteryCapacity = batteryStatus.second; + +#elif defined(__linux__) + const std::string sysEntry {"/sys/class/power_supply"}; + std::string batteryDir; + auto entries {Utils::FileSystem::getDirContent(sysEntry, false)}; + if (std::find(entries.cbegin(), entries.cend(), sysEntry + "/BAT0") != entries.cend()) + batteryDir = sysEntry + "/BAT0"; + else if (std::find(entries.cbegin(), entries.cend(), sysEntry + "/BAT1") != entries.cend()) + batteryDir = sysEntry + "/BAT1"; + else if (std::find(entries.cbegin(), entries.cend(), sysEntry + "/battery") != entries.cend()) + batteryDir = sysEntry + "/battery"; + + if (!Utils::FileSystem::exists(batteryDir + "/status")) + hasBattery = false; + if (!Utils::FileSystem::exists(batteryDir + "/capacity")) + hasBattery = false; + + if (hasBattery) { + std::string batteryStatusValue; + std::string batteryCapacityValue; + std::ifstream fileStream; + fileStream.open(batteryDir + "/status"); + getline(fileStream, batteryStatusValue); + batteryStatusValue = Utils::String::toLower(batteryStatusValue); + fileStream.close(); + + if (batteryStatusValue != "discharging") + batteryCharging = true; + + fileStream.open(batteryDir + "/capacity"); + getline(fileStream, batteryCapacityValue); + fileStream.close(); + + batteryCapacity = std::stoi(batteryCapacityValue); + } +#endif + + mHasBattery = hasBattery; + mBatteryCharging = batteryCharging; + mBatteryCapacity = batteryCapacity; +} diff --git a/es-core/src/SystemStatus.h b/es-core/src/SystemStatus.h new file mode 100644 index 000000000..789b4fdae --- /dev/null +++ b/es-core/src/SystemStatus.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +// +// ES-DE Frontend +// SystemStatus.h +// +// Queries system status information from the operating system. +// This includes Bluetooth, Wi-Fi, cellular and battery. +// + +#ifndef ES_CORE_SYSTEM_STATUS_H +#define ES_CORE_SYSTEM_STATUS_H + +#include +#include +#include + +class SystemStatus +{ +public: + ~SystemStatus(); + static SystemStatus& getInstance(); + + void setCheckFlags(); + void setPolling(const bool state); + void pollImmediately() { mPollImmediately = true; } + + struct Status { + bool hasBluetooth; + bool hasWifi; + bool hasCellular; + bool hasBattery; + bool batteryCharging; + int batteryCapacity; + Status() + : hasBluetooth {false} + , hasWifi {false} + , hasCellular {false} + , hasBattery {false} + , batteryCharging {false} + , batteryCapacity {0} + { + } + }; + + Status getStatus(); + +private: + SystemStatus() noexcept; + + void pollStatus(); + + void getStatusBluetooth(); + void getStatusWifi(); + void getStatusCellular(); + void getStatusBattery(); + + bool mCheckBluetooth; + bool mCheckWifi; + bool mCheckCellular; + bool mCheckBattery; + + std::unique_ptr mPollThread; + Status mStatus; + std::mutex mStatusMutex; + + std::atomic mExitPolling; + std::atomic mPollImmediately; + + std::atomic mHasBluetooth; + std::atomic mHasWifi; + std::atomic mHasCellular; + std::atomic mHasBattery; + std::atomic mBatteryCharging; + std::atomic mBatteryCapacity; +}; + +#endif // ES_CORE_SYSTEM_STATUS_H