From 8c33b0bb5df5b755a73434ae74a15edc9ceb4c82 Mon Sep 17 00:00:00 2001 From: Maufeat Date: Wed, 18 Jun 2025 08:34:54 +0000 Subject: [PATCH] Add Device Power State (Windows, Linux, Mac and Android) (#197) Uses native power state methods to display battery percentage and charging state correctly. Mainly for qlaunch. Tested on Windows, Linux. Mac and Android Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/197 Co-authored-by: Maufeat Co-committed-by: Maufeat --- .../java/org/yuzu/yuzu_emu/NativeLibrary.kt | 11 +- .../java/org/yuzu/yuzu_emu/YuzuApplication.kt | 5 + .../yuzu_emu/activities/EmulationActivity.kt | 7 +- .../yuzu/yuzu_emu/utils/PowerStateUpdater.kt | 46 ++++++++ .../yuzu/yuzu_emu/utils/PowerStateUtils.kt | 45 ++++++++ src/android/app/src/main/jni/native.cpp | 23 +++- src/common/CMakeLists.txt | 2 + src/common/device_power_state.cpp | 102 ++++++++++++++++++ src/common/device_power_state.h | 14 +++ src/core/hle/service/ptm/psm.cpp | 22 +++- src/core/hle/service/ptm/psm.h | 6 +- 11 files changed, 269 insertions(+), 14 deletions(-) create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUpdater.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUtils.kt create mode 100644 src/common/device_power_state.cpp create mode 100644 src/common/device_power_state.h diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index c8f44c060e..3435d08132 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -1,9 +1,9 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -// SPDX-FileCopyrightText: 2025 Eden Emulator Project -// SPDX-License-Identifier: GPL-3.0-or-later - package org.yuzu.yuzu_emu @@ -489,4 +489,9 @@ object NativeLibrary { * Checks if all necessary keys are present for decryption */ external fun areKeysPresent(): Boolean + + /** + * Updates the device power state to global variables + */ + external fun updatePowerState(percentage: Int, isCharging: Boolean, hasBattery: Boolean) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt index 72943f33e0..664442b20e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -13,6 +16,7 @@ import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DocumentsTree import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.Log +import org.yuzu.yuzu_emu.utils.PowerStateUpdater fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir @@ -40,6 +44,7 @@ class YuzuApplication : Application() { GpuDriverHelper.initializeDriverParameters() NativeInput.reloadInputDevices() NativeLibrary.logDeviceInfo() + PowerStateUpdater.start() Log.logDeviceInfo() createNotificationChannels() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index 8622aa658e..40200931b7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -1,9 +1,9 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -// SPDX-FileCopyrightText: 2025 Eden Emulator Project -// SPDX-License-Identifier: GPL-3.0-or-later - package org.yuzu.yuzu_emu.activities @@ -58,6 +58,7 @@ import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NfcReader import org.yuzu.yuzu_emu.utils.ParamPackage import org.yuzu.yuzu_emu.utils.ThemeHelper +import org.yuzu.yuzu_emu.utils.PowerStateUtils import java.text.NumberFormat import kotlin.math.roundToInt diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUpdater.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUpdater.kt new file mode 100644 index 0000000000..58d2d1e7ed --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUpdater.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + + +package org.yuzu.yuzu_emu.utils + +import android.os.Handler +import android.os.Looper +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.YuzuApplication + +object PowerStateUpdater { + + private lateinit var handler: Handler + private lateinit var runnable: Runnable + private const val UPDATE_INTERVAL_MS = 1000L + private var isStarted = false + + fun start() { + + if (isStarted) { + return + } + + val context = YuzuApplication.appContext + + handler = Handler(Looper.getMainLooper()) + runnable = Runnable { + val info = PowerStateUtils.getBatteryInfo(context) + NativeLibrary.updatePowerState(info[0], info[1] == 1, info[2] == 1) + handler.postDelayed(runnable, UPDATE_INTERVAL_MS) + } + handler.post(runnable) + isStarted = true + } + + fun stop() { + if (!isStarted) { + return + } + if (::handler.isInitialized) { + handler.removeCallbacks(runnable) + } + isStarted = false + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUtils.kt new file mode 100644 index 0000000000..86e3d9c785 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PowerStateUtils.kt @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.utils + +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.os.Build + +object PowerStateUtils { + + @JvmStatic + fun getBatteryInfo(context: Context?): IntArray { + + if (context == null) { + return intArrayOf(0, 0, 0) // Percentage, IsCharging, HasBattery + } + + val results = intArrayOf(100, 0, 1) + val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + val batteryStatusIntent: Intent? = context.registerReceiver(null, iFilter) + + if (batteryStatusIntent != null) { + val present = batteryStatusIntent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true) + if (!present) { + results[2] = 0; results[0] = 0; results[1] = 0; return results + } + results[2] = 1 + val level = batteryStatusIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = batteryStatusIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + if (level != -1 && scale != -1 && scale != 0) { + results[0] = (level.toFloat() / scale.toFloat() * 100.0f).toInt() + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val bm = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager? + results[0] = bm?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) ?: 100 + } + val status = batteryStatusIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + results[1] = if (status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL) 1 else 0 + } + + return results + } +} \ No newline at end of file diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 7a450c068a..309f298789 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -1,9 +1,9 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -// SPDX-FileCopyrightText: 2025 Eden Emulator Project -// SPDX-License-Identifier: GPL-3.0-or-later - #include #include @@ -79,6 +79,11 @@ static EmulationSession s_instance; std::unique_ptr multiplayer{nullptr}; std::shared_ptr announce_multiplayer_session; +//Power Status default values +std::atomic g_battery_percentage = {100}; +std::atomic g_is_charging = {false}; +std::atomic g_has_battery = {true}; + EmulationSession::EmulationSession() { m_vfs = std::make_shared(); } @@ -1014,4 +1019,16 @@ JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayUnb JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) { multiplayer->NetPlayUnbanUser(Common::Android::GetJString(env, username)); } + +JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_updatePowerState( + JNIEnv* env, + jobject, + jint percentage, + jboolean isCharging, + jboolean hasBattery) { + + g_battery_percentage.store(percentage, std::memory_order_relaxed); + g_is_charging.store(isCharging, std::memory_order_relaxed); + g_has_battery.store(hasBattery, std::memory_order_relaxed); +} } // extern "C" diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 433b0908ad..d7097c8cd7 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -42,6 +42,8 @@ add_library(common STATIC demangle.h detached_tasks.cpp detached_tasks.h + device_power_state.cpp + device_power_state.h div_ceil.h dynamic_library.cpp dynamic_library.h diff --git a/src/common/device_power_state.cpp b/src/common/device_power_state.cpp new file mode 100644 index 0000000000..2dfa7dc305 --- /dev/null +++ b/src/common/device_power_state.cpp @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "device_power_state.h" + +#if defined(_WIN32) +#include + +#elif defined(__ANDROID__) +#include +extern std::atomic g_battery_percentage; +extern std::atomic g_is_charging; +extern std::atomic g_has_battery; + +#elif defined(__APPLE__) +#include +#if TARGET_OS_MAC +#include +#include +#endif + +#elif defined(__linux__) +#include +#include +#include +#endif + +namespace Common { + + PowerStatus GetPowerStatus() + { + PowerStatus info; + +#if defined(_WIN32) + SYSTEM_POWER_STATUS status; + if (GetSystemPowerStatus(&status)) { + if (status.BatteryFlag == 128) { + info.has_battery = false; + } else { + info.percentage = status.BatteryLifePercent; + info.charging = (status.BatteryFlag & 8) != 0; + } + } else { + info.has_battery = false; + } + +#elif defined(__ANDROID__) + info.percentage = g_battery_percentage.load(std::memory_order_relaxed); + info.charging = g_is_charging.load(std::memory_order_relaxed); + info.has_battery = g_has_battery.load(std::memory_order_relaxed); + +#elif defined(__APPLE__) && TARGET_OS_MAC + CFTypeRef info_ref = IOPSCopyPowerSourcesInfo(); + CFArrayRef sources = IOPSCopyPowerSourcesList(info_ref); + if (CFArrayGetCount(sources) > 0) { + CFDictionaryRef battery = + IOPSGetPowerSourceDescription(info_ref, CFArrayGetValueAtIndex(sources, 0)); + + CFNumberRef curNum = + (CFNumberRef)CFDictionaryGetValue(battery, CFSTR(kIOPSCurrentCapacityKey)); + CFNumberRef maxNum = + (CFNumberRef)CFDictionaryGetValue(battery, CFSTR(kIOPSMaxCapacityKey)); + int cur = 0, max = 0; + CFNumberGetValue(curNum, kCFNumberIntType, &cur); + CFNumberGetValue(maxNum, kCFNumberIntType, &max); + + if (max > 0) + info.percentage = (cur * 100) / max; + + CFBooleanRef isCharging = + (CFBooleanRef)CFDictionaryGetValue(battery, CFSTR(kIOPSIsChargingKey)); + info.charging = CFBooleanGetValue(isCharging); + } else { + info.has_battery = false; + } + CFRelease(sources); + CFRelease(info_ref); + +#elif defined(__linux__) + const char* battery_path = "/sys/class/power_supply/BAT0/"; + + std::ifstream capFile(std::string(battery_path) + "capacity"); + if (capFile) { + capFile >> info.percentage; + } + else { + info.has_battery = false; + } + + std::ifstream statFile(std::string(battery_path) + "status"); + if (statFile) { + std::string status; + std::getline(statFile, status); + info.charging = (status == "Charging"); + } +#else + info.has_battery = false; +#endif + + return info; + } +} diff --git a/src/common/device_power_state.h b/src/common/device_power_state.h new file mode 100644 index 0000000000..3f214308bf --- /dev/null +++ b/src/common/device_power_state.h @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +namespace Common { +struct PowerStatus { + int percentage = -1; + bool charging = false; + bool has_battery = true; +}; + +PowerStatus GetPowerStatus(); +} // namespace Common \ No newline at end of file diff --git a/src/core/hle/service/ptm/psm.cpp b/src/core/hle/service/ptm/psm.cpp index 136313d7b1..013cce3579 100644 --- a/src/core/hle/service/ptm/psm.cpp +++ b/src/core/hle/service/ptm/psm.cpp @@ -1,8 +1,12 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include +#include "common/device_power_state.h" #include "common/logging/log.h" #include "core/core.h" #include "core/hle/kernel/k_event.h" @@ -148,17 +152,31 @@ PSM::~PSM() = default; void PSM::GetBatteryChargePercentage(HLERequestContext& ctx) { LOG_DEBUG(Service_PTM, "called"); + u32 percentage = 100; + + Common::PowerStatus power_status = Common::GetPowerStatus(); + + if (power_status.has_battery && power_status.percentage >= 0) { + percentage = static_cast(power_status.percentage); + } + IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.Push(battery_charge_percentage); + rb.Push(percentage); } void PSM::GetChargerType(HLERequestContext& ctx) { LOG_DEBUG(Service_PTM, "called"); + ChargerType charger = ChargerType::Unplugged; + Common::PowerStatus power_status = Common::GetPowerStatus(); + if (power_status.has_battery && power_status.charging) { + charger = ChargerType::RegularCharger; + } + IPC::ResponseBuilder rb{ctx, 3}; rb.Push(ResultSuccess); - rb.PushEnum(charger_type); + rb.PushEnum(charger); } void PSM::OpenSession(HLERequestContext& ctx) { diff --git a/src/core/hle/service/ptm/psm.h b/src/core/hle/service/ptm/psm.h index fa47919e55..2853a45753 100644 --- a/src/core/hle/service/ptm/psm.h +++ b/src/core/hle/service/ptm/psm.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -23,9 +26,6 @@ private: void GetBatteryChargePercentage(HLERequestContext& ctx); void GetChargerType(HLERequestContext& ctx); void OpenSession(HLERequestContext& ctx); - - u32 battery_charge_percentage{100}; - ChargerType charger_type{ChargerType::RegularCharger}; }; } // namespace Service::PTM