Add Device Power State (Windows, Linux, Mac and Android) (#197)
All checks were successful
eden-build / source (push) Successful in 3m27s
eden-build / android (push) Successful in 22m59s
eden-build / linux (push) Successful in 21m36s
eden-build / windows (msvc) (push) Successful in 29m29s

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 <sahyno1996@gmail.com>
Co-committed-by: Maufeat <sahyno1996@gmail.com>
This commit is contained in:
Maufeat 2025-06-18 08:34:54 +00:00 committed by Maufeat
parent 6bf5ae700a
commit 8c33b0bb5d
11 changed files with 269 additions and 14 deletions

View file

@ -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-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // 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 package org.yuzu.yuzu_emu
@ -489,4 +489,9 @@ object NativeLibrary {
* Checks if all necessary keys are present for decryption * Checks if all necessary keys are present for decryption
*/ */
external fun areKeysPresent(): Boolean external fun areKeysPresent(): Boolean
/**
* Updates the device power state to global variables
*/
external fun updatePowerState(percentage: Int, isCharging: Boolean, hasBattery: Boolean)
} }

View file

@ -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-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // 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.DocumentsTree
import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.PowerStateUpdater
fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
@ -40,6 +44,7 @@ class YuzuApplication : Application() {
GpuDriverHelper.initializeDriverParameters() GpuDriverHelper.initializeDriverParameters()
NativeInput.reloadInputDevices() NativeInput.reloadInputDevices()
NativeLibrary.logDeviceInfo() NativeLibrary.logDeviceInfo()
PowerStateUpdater.start()
Log.logDeviceInfo() Log.logDeviceInfo()
createNotificationChannels() createNotificationChannels()

View file

@ -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-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // 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 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.NfcReader
import org.yuzu.yuzu_emu.utils.ParamPackage import org.yuzu.yuzu_emu.utils.ParamPackage
import org.yuzu.yuzu_emu.utils.ThemeHelper import org.yuzu.yuzu_emu.utils.ThemeHelper
import org.yuzu.yuzu_emu.utils.PowerStateUtils
import java.text.NumberFormat import java.text.NumberFormat
import kotlin.math.roundToInt import kotlin.math.roundToInt

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <codecvt> #include <codecvt>
#include <locale> #include <locale>
@ -79,6 +79,11 @@ static EmulationSession s_instance;
std::unique_ptr<AndroidMultiplayer> multiplayer{nullptr}; std::unique_ptr<AndroidMultiplayer> multiplayer{nullptr};
std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session; std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
//Power Status default values
std::atomic<int> g_battery_percentage = {100};
std::atomic<bool> g_is_charging = {false};
std::atomic<bool> g_has_battery = {true};
EmulationSession::EmulationSession() { EmulationSession::EmulationSession() {
m_vfs = std::make_shared<FileSys::RealVfsFilesystem>(); m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
} }
@ -1014,4 +1019,16 @@ JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayUnb
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) { JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
multiplayer->NetPlayUnbanUser(Common::Android::GetJString(env, 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" } // extern "C"

View file

@ -42,6 +42,8 @@ add_library(common STATIC
demangle.h demangle.h
detached_tasks.cpp detached_tasks.cpp
detached_tasks.h detached_tasks.h
device_power_state.cpp
device_power_state.h
div_ceil.h div_ceil.h
dynamic_library.cpp dynamic_library.cpp
dynamic_library.h dynamic_library.h

View file

@ -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 <windows.h>
#elif defined(__ANDROID__)
#include <atomic>
extern std::atomic<int> g_battery_percentage;
extern std::atomic<bool> g_is_charging;
extern std::atomic<bool> g_has_battery;
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#if TARGET_OS_MAC
#include <IOKit/ps/IOPSKeys.h>
#include <IOKit/ps/IOPowerSources.h>
#endif
#elif defined(__linux__)
#include <fstream>
#include <string>
#include <dirent.h>
#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;
}
}

View file

@ -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

View file

@ -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-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <memory> #include <memory>
#include "common/device_power_state.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "core/core.h" #include "core/core.h"
#include "core/hle/kernel/k_event.h" #include "core/hle/kernel/k_event.h"
@ -148,17 +152,31 @@ PSM::~PSM() = default;
void PSM::GetBatteryChargePercentage(HLERequestContext& ctx) { void PSM::GetBatteryChargePercentage(HLERequestContext& ctx) {
LOG_DEBUG(Service_PTM, "called"); 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<u32>(power_status.percentage);
}
IPC::ResponseBuilder rb{ctx, 3}; IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
rb.Push<u32>(battery_charge_percentage); rb.Push<u32>(percentage);
} }
void PSM::GetChargerType(HLERequestContext& ctx) { void PSM::GetChargerType(HLERequestContext& ctx) {
LOG_DEBUG(Service_PTM, "called"); 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}; IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess); rb.Push(ResultSuccess);
rb.PushEnum(charger_type); rb.PushEnum(charger);
} }
void PSM::OpenSession(HLERequestContext& ctx) { void PSM::OpenSession(HLERequestContext& ctx) {

View file

@ -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-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -23,9 +26,6 @@ private:
void GetBatteryChargePercentage(HLERequestContext& ctx); void GetBatteryChargePercentage(HLERequestContext& ctx);
void GetChargerType(HLERequestContext& ctx); void GetChargerType(HLERequestContext& ctx);
void OpenSession(HLERequestContext& ctx); void OpenSession(HLERequestContext& ctx);
u32 battery_charge_percentage{100};
ChargerType charger_type{ChargerType::RegularCharger};
}; };
} // namespace Service::PTM } // namespace Service::PTM