diff --git a/wpeview/src/main/cpp/CMakeLists.txt b/wpeview/src/main/cpp/CMakeLists.txt index 4c129a546..6441cb5ad 100644 --- a/wpeview/src/main/cpp/CMakeLists.txt +++ b/wpeview/src/main/cpp/CMakeLists.txt @@ -132,7 +132,8 @@ add_library( Runtime/WKWebContext.cpp Runtime/WKSettings.cpp Runtime/WKWebsiteDataManager.cpp - Runtime/WKWebView.cpp) + Runtime/WKWebView.cpp + Runtime/WKPowerProfileMonitor.cpp) target_configure_quality(WPEAndroidRuntime) target_compile_definitions(WPEAndroidRuntime PRIVATE WPE_ENABLE_PROCESS) target_link_libraries( diff --git a/wpeview/src/main/cpp/Runtime/EntryPoint.cpp b/wpeview/src/main/cpp/Runtime/EntryPoint.cpp index df4c17b49..925fa8347 100644 --- a/wpeview/src/main/cpp/Runtime/EntryPoint.cpp +++ b/wpeview/src/main/cpp/Runtime/EntryPoint.cpp @@ -27,6 +27,7 @@ #include "WKWebContext.h" #include "WKWebView.h" #include "WKWebsiteDataManager.h" +#include "WKPowerProfileMonitor.h" extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* javaVM, void* /*reserved*/) { @@ -41,6 +42,8 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* javaVM, void* /*reserved*/) WKWebsiteDataManager::configureJNIMappings(); WKWebView::configureJNIMappings(); WKSettings::configureJNIMappings(); + WKPowerProfileMonitor::configureJNIMappings(); + WKPowerProfileMonitor::registerExtension(); return JNI::VERSION; } catch (const std::exception& e) { diff --git a/wpeview/src/main/cpp/Runtime/WKPowerProfileMonitor.cpp b/wpeview/src/main/cpp/Runtime/WKPowerProfileMonitor.cpp new file mode 100644 index 000000000..fd9d73799 --- /dev/null +++ b/wpeview/src/main/cpp/Runtime/WKPowerProfileMonitor.cpp @@ -0,0 +1,226 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * Author: maceip + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "WKPowerProfileMonitor.h" + +#include "JNI/JNI.h" +#include "Logging.h" + +#include + +// NDK Thermal API (API level 30+) - always include, use runtime detection +#include + +struct _WPEAndroidPowerProfileMonitor { + GObject parent; + + AThermalManager* thermalManager; + + // Thread-safe flags: NDK thermal callbacks occur on Binder threads, + // JNI calls may be on UI thread. Using gint with g_atomic_int_* for + // GLib-compatible atomic operations. + gint isBatterySaverActive; + gint isThermalThrottling; +}; + +enum { PROP_0, PROP_POWER_SAVER_ENABLED }; + +static WPEAndroidPowerProfileMonitor* s_singleton = nullptr; + +static void wpe_android_power_profile_monitor_iface_init(GPowerProfileMonitorInterface*); + +G_DEFINE_FINAL_TYPE_WITH_CODE(WPEAndroidPowerProfileMonitor, wpe_android_power_profile_monitor, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_POWER_PROFILE_MONITOR, wpe_android_power_profile_monitor_iface_init)) + +static void scheduleUpdateOnMainThread(WPEAndroidPowerProfileMonitor* self) +{ + g_object_ref(self); + g_main_context_invoke( + nullptr, + +[](gpointer userData) -> gboolean { + auto* monitor = WPE_ANDROID_POWER_PROFILE_MONITOR(userData); + bool isPowerSaverEnabled + = g_atomic_int_get(&monitor->isBatterySaverActive) || g_atomic_int_get(&monitor->isThermalThrottling); + Logging::logDebug("WPEAndroidPowerProfileMonitor: power-saver-enabled=%s (battery=%s, thermal=%s)", + isPowerSaverEnabled ? "true" : "false", + g_atomic_int_get(&monitor->isBatterySaverActive) ? "true" : "false", + g_atomic_int_get(&monitor->isThermalThrottling) ? "true" : "false"); + + g_object_notify(G_OBJECT(monitor), "power-saver-enabled"); + g_object_unref(monitor); + return G_SOURCE_REMOVE; + }, + self); +} + +static void onThermalStatusChanged(void* data, AThermalStatus status) +{ + auto* self = WPE_ANDROID_POWER_PROFILE_MONITOR(data); + gint throttling = (status >= ATHERMAL_STATUS_SEVERE) ? TRUE : FALSE; + + Logging::logDebug( + "WPEAndroidPowerProfileMonitor: thermal status changed to %d, throttling=%s", status, throttling ? "true" : "false"); + + if (g_atomic_int_get(&self->isThermalThrottling) != throttling) { + g_atomic_int_set(&self->isThermalThrottling, throttling); + scheduleUpdateOnMainThread(self); + } +} + +static void setBatterySaver(gboolean isPowerSaveMode) +{ + if (s_singleton == nullptr) { + Logging::logDebug("WPEAndroidPowerProfileMonitor: setBatterySaver called before init, ignoring"); + return; + } + + gint newValue = isPowerSaveMode ? TRUE : FALSE; + if (g_atomic_int_get(&s_singleton->isBatterySaverActive) != newValue) { + Logging::logDebug("WPEAndroidPowerProfileMonitor: battery saver changed to %s", newValue ? "true" : "false"); + g_atomic_int_set(&s_singleton->isBatterySaverActive, newValue); + scheduleUpdateOnMainThread(s_singleton); + } +} + +DECLARE_JNI_CLASS_SIGNATURE(JNIWPEPowerMonitor, "org/wpewebkit/wpe/WPEPowerMonitor"); + +class JNIWPEPowerMonitorCache final : public JNI::TypedClass { +public: + JNIWPEPowerMonitorCache() + : JNI::TypedClass(true) + { + registerNativeMethods( + JNI::StaticNativeMethod("nativeOnPowerSaveModeChanged", nativeOnPowerSaveModeChanged)); + } + +private: + static void nativeOnPowerSaveModeChanged(JNIEnv*, jclass, jboolean isPowerSave) + { + bool newVal = (isPowerSave != JNI_FALSE); + Logging::logDebug("WPEAndroidPowerProfileMonitor: nativeOnPowerSaveModeChanged(%s)", newVal ? "true" : "false"); + setBatterySaver(newVal ? TRUE : FALSE); + } +}; + +static const JNIWPEPowerMonitorCache& getJNIWPEPowerMonitorCache() +{ + static const JNIWPEPowerMonitorCache s_singleton; + return s_singleton; +} + +static void wpe_android_power_profile_monitor_iface_init(GPowerProfileMonitorInterface*) +{ + // Interface implemented via "power-saver-enabled" property override +} + +static void wpe_android_power_profile_monitor_get_property(GObject* object, guint propId, GValue* value, GParamSpec* pspec) +{ + auto* self = WPE_ANDROID_POWER_PROFILE_MONITOR(object); + + switch (propId) { + case PROP_POWER_SAVER_ENABLED: + g_value_set_boolean(value, + static_cast( + g_atomic_int_get(&self->isBatterySaverActive) || g_atomic_int_get(&self->isThermalThrottling))); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, pspec); + break; + } +} + +static void wpe_android_power_profile_monitor_init(WPEAndroidPowerProfileMonitor* self) +{ + Logging::logDebug("WPEAndroidPowerProfileMonitor: init(%p)", static_cast(self)); + + s_singleton = self; + self->thermalManager = nullptr; + g_atomic_int_set(&self->isBatterySaverActive, FALSE); + g_atomic_int_set(&self->isThermalThrottling, FALSE); + + if (__builtin_available(android 30, *)) { + self->thermalManager = AThermal_acquireManager(); + if (self->thermalManager != nullptr) { + Logging::logDebug("WPEAndroidPowerProfileMonitor: thermal manager acquired"); + + int result = AThermal_registerThermalStatusListener(self->thermalManager, onThermalStatusChanged, self); + if (result == 0) { + AThermalStatus status = AThermal_getCurrentThermalStatus(self->thermalManager); + g_atomic_int_set(&self->isThermalThrottling, (status >= ATHERMAL_STATUS_SEVERE) ? TRUE : FALSE); + Logging::logDebug("WPEAndroidPowerProfileMonitor: initial thermal status=%d, throttling=%s", + static_cast(status), g_atomic_int_get(&self->isThermalThrottling) ? "true" : "false"); + } else { + Logging::logError("WPEAndroidPowerProfileMonitor: failed to register thermal listener: %d", result); + } + } else { + Logging::logDebug("WPEAndroidPowerProfileMonitor: thermal manager not available"); + } + } else { + Logging::logDebug("WPEAndroidPowerProfileMonitor: thermal API not available (requires Android 11+)"); + } +} + +static void wpe_android_power_profile_monitor_dispose(GObject* object) +{ + auto* self = WPE_ANDROID_POWER_PROFILE_MONITOR(object); + Logging::logDebug("WPEAndroidPowerProfileMonitor: dispose(%p)", static_cast(object)); + + if (__builtin_available(android 30, *)) { + if (self->thermalManager != nullptr) { + AThermal_unregisterThermalStatusListener(self->thermalManager, onThermalStatusChanged, self); + AThermal_releaseManager(self->thermalManager); + self->thermalManager = nullptr; + } + } + + if (s_singleton == self) + s_singleton = nullptr; + + G_OBJECT_CLASS(wpe_android_power_profile_monitor_parent_class)->dispose(object); +} + +static void wpe_android_power_profile_monitor_class_init(WPEAndroidPowerProfileMonitorClass* klass) +{ + GObjectClass* objectClass = G_OBJECT_CLASS(klass); + objectClass->dispose = wpe_android_power_profile_monitor_dispose; + objectClass->get_property = wpe_android_power_profile_monitor_get_property; + + g_object_class_override_property(objectClass, PROP_POWER_SAVER_ENABLED, "power-saver-enabled"); +} + +void WKPowerProfileMonitor::configureJNIMappings() +{ + Logging::logDebug("WKPowerProfileMonitor: configureJNIMappings"); + getJNIWPEPowerMonitorCache(); +} + +void WKPowerProfileMonitor::registerExtension() +{ + Logging::logDebug("WKPowerProfileMonitor: registerExtension"); + + g_type_ensure(WPE_TYPE_ANDROID_POWER_PROFILE_MONITOR); + + if (g_io_extension_point_lookup(G_POWER_PROFILE_MONITOR_EXTENSION_POINT_NAME) == nullptr) + g_io_extension_point_register(G_POWER_PROFILE_MONITOR_EXTENSION_POINT_NAME); + + g_io_extension_point_implement( + G_POWER_PROFILE_MONITOR_EXTENSION_POINT_NAME, WPE_TYPE_ANDROID_POWER_PROFILE_MONITOR, "android", 10); + + Logging::logDebug("WKPowerProfileMonitor: registered as GPowerProfileMonitor extension"); +} diff --git a/wpeview/src/main/cpp/Runtime/WKPowerProfileMonitor.h b/wpeview/src/main/cpp/Runtime/WKPowerProfileMonitor.h new file mode 100644 index 000000000..2502342c7 --- /dev/null +++ b/wpeview/src/main/cpp/Runtime/WKPowerProfileMonitor.h @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * Author: maceip + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define WPE_TYPE_ANDROID_POWER_PROFILE_MONITOR (wpe_android_power_profile_monitor_get_type()) +G_DECLARE_FINAL_TYPE( + WPEAndroidPowerProfileMonitor, wpe_android_power_profile_monitor, WPE, ANDROID_POWER_PROFILE_MONITOR, GObject) + +G_END_DECLS + +/** + * WKPowerProfileMonitor implements GPowerProfileMonitor for Android. + * + * Aggregates two signals to determine "Low Power Mode": + * 1. Thermal Status (via NDK AThermalManager, API 30+) + * 2. Battery Saver Mode (via Java PowerManager broadcast) + * + * When either condition indicates power constraints, WebKit's LowPowerModeNotifierGLib + * sees "power-saver-enabled" as TRUE, causing reduced timer precision, stopped + * smooth animations, and throttled background tabs. + */ +class WKPowerProfileMonitor { +public: + static void configureJNIMappings(); + static void registerExtension(); +}; diff --git a/wpeview/src/main/cpp/Runtime/WPEAndroidPowerProfileMonitor.cpp b/wpeview/src/main/cpp/Runtime/WPEAndroidPowerProfileMonitor.cpp new file mode 100644 index 000000000..787eac83b --- /dev/null +++ b/wpeview/src/main/cpp/Runtime/WPEAndroidPowerProfileMonitor.cpp @@ -0,0 +1,262 @@ +/** + * Copyright (C) 2025 maceip + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "WPEAndroidPowerProfileMonitor.h" + +#include "JNI/JNI.h" +#include "Logging.h" + +#include +#include + +// NDK Thermal API (API level 30+) - always include, use runtime detection +#include + +/** + * WPEAndroidPowerProfileMonitor implements GPowerProfileMonitor for Android. + * + * Aggregates two signals to determine "Low Power Mode": + * 1. Thermal Status (via NDK AThermalManager on API 30+) + * 2. Battery Saver Mode (via Java PowerManager broadcast) + * + * When either thermal throttling is active (SEVERE or higher) or Battery Saver + * mode is enabled, the monitor reports power-saver-enabled=TRUE to WebKit. + */ +struct _WPEAndroidPowerMonitor { + GObject parentInstance; + + AThermalManager* thermalManager; + + // Thread-safe flags: NDK thermal callbacks occur on Binder threads, + // JNI calls may be on UI thread + std::atomic isBatterySaverActive; + std::atomic isThermalThrottling; +}; + +enum { PROP_0, PROP_POWER_SAVER_ENABLED, N_PROPERTIES }; + +static WPEAndroidPowerMonitor* s_singleton = nullptr; + +static void wpeAndroidPowerProfileMonitorIfaceInit(GPowerProfileMonitorInterface* iface); + +G_DEFINE_FINAL_TYPE_WITH_CODE(WPEAndroidPowerMonitor, wpe_android_power_profile_monitor, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_POWER_PROFILE_MONITOR, wpeAndroidPowerProfileMonitorIfaceInit)) + +static void updatePowerStateOnMainThread(WPEAndroidPowerMonitor* self) +{ + bool isPowerSaverEnabled = self->isBatterySaverActive.load() || self->isThermalThrottling.load(); + Logging::logDebug("WPEAndroidPowerProfileMonitor: power-saver-enabled=%s (battery=%s, thermal=%s)", + isPowerSaverEnabled ? "true" : "false", self->isBatterySaverActive.load() ? "true" : "false", + self->isThermalThrottling.load() ? "true" : "false"); + + g_object_notify(G_OBJECT(self), "power-saver-enabled"); +} + +// AThermalStatus values: +// ATHERMAL_STATUS_NONE = 0, LIGHT = 1, MODERATE = 2, SEVERE = 3, +// CRITICAL = 4, EMERGENCY = 5, SHUTDOWN = 6 +static void onThermalStatusChanged(void* data, AThermalStatus status) +{ + auto* self = WPE_ANDROID_POWER_PROFILE_MONITOR(data); + bool throttling = (status >= ATHERMAL_STATUS_SEVERE); + + Logging::logDebug("WPEAndroidPowerProfileMonitor::onThermalStatusChanged(%d, throttling=%s)", + static_cast(status), throttling ? "true" : "false"); + + if (self->isThermalThrottling.load() != throttling) { + self->isThermalThrottling.store(throttling); + g_main_context_invoke( + nullptr, + +[](gpointer userData) -> gboolean { + updatePowerStateOnMainThread(WPE_ANDROID_POWER_PROFILE_MONITOR(userData)); + return G_SOURCE_REMOVE; + }, + self); + } +} + +DECLARE_JNI_CLASS_SIGNATURE(JNIWPEPowerMonitor, "org/wpewebkit/wpe/WPEPowerMonitor"); + +class JNIWPEPowerMonitorCache final : public JNI::TypedClass { +public: + JNIWPEPowerMonitorCache() + : JNI::TypedClass(true) + { + registerNativeMethods( + JNI::StaticNativeMethod("nativeOnPowerSaveModeChanged", nativeOnPowerSaveModeChanged)); + } + +private: + static void nativeOnPowerSaveModeChanged(JNIEnv*, jclass, jboolean isPowerSave) + { + bool newVal = (isPowerSave != JNI_FALSE); + Logging::logDebug("WPEAndroidPowerProfileMonitor::nativeOnPowerSaveModeChanged(%s)", newVal ? "true" : "false"); + wpe_android_power_profile_monitor_set_battery_saver(newVal ? TRUE : FALSE); + } +}; + +static const JNIWPEPowerMonitorCache& getJNIWPEPowerMonitorCache() +{ + static const JNIWPEPowerMonitorCache s_singleton; + return s_singleton; +} + +static void wpeAndroidPowerProfileMonitorIfaceInit(GPowerProfileMonitorInterface* iface) +{ + // Interface implemented via "power-saver-enabled" property override + (void)iface; +} + +static void wpeAndroidPowerProfileMonitorGetProperty(GObject* object, guint propId, GValue* value, GParamSpec* pspec) +{ + auto* self = WPE_ANDROID_POWER_PROFILE_MONITOR(object); + + switch (propId) { + case PROP_POWER_SAVER_ENABLED: + g_value_set_boolean( + value, static_cast(self->isBatterySaverActive.load() || self->isThermalThrottling.load())); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propId, pspec); + break; + } +} + +static void wpe_android_power_profile_monitor_init(WPEAndroidPowerMonitor* self) +{ + Logging::logDebug("WPEAndroidPowerProfileMonitor::init(%p)", static_cast(self)); + + s_singleton = self; + self->thermalManager = nullptr; + self->isBatterySaverActive.store(false); + self->isThermalThrottling.store(false); + + if (__builtin_available(android 30, *)) { + self->thermalManager = AThermal_acquireManager(); + if (self->thermalManager != nullptr) { + Logging::logDebug("WPEAndroidPowerProfileMonitor: Thermal manager acquired"); + + int result = AThermal_registerThermalStatusListener(self->thermalManager, onThermalStatusChanged, self); + if (result == 0) { + AThermalStatus status = AThermal_getCurrentThermalStatus(self->thermalManager); + self->isThermalThrottling.store(status >= ATHERMAL_STATUS_SEVERE); + Logging::logDebug("WPEAndroidPowerProfileMonitor: Initial thermal status=%d, throttling=%s", + static_cast(status), self->isThermalThrottling.load() ? "true" : "false"); + } else { + Logging::logError("WPEAndroidPowerProfileMonitor: Failed to register thermal listener: %d", result); + } + } else { + Logging::logDebug("WPEAndroidPowerProfileMonitor: Thermal manager not available"); + } + } else { + Logging::logDebug("WPEAndroidPowerProfileMonitor: Thermal API not available (requires Android 11+)"); + } +} + +static void wpeAndroidPowerProfileMonitorDispose(GObject* object) +{ + auto* self = WPE_ANDROID_POWER_PROFILE_MONITOR(object); + Logging::logDebug("WPEAndroidPowerProfileMonitor::dispose(%p)", static_cast(object)); + + if (__builtin_available(android 30, *)) { + if (self->thermalManager != nullptr) { + AThermal_unregisterThermalStatusListener(self->thermalManager, onThermalStatusChanged, self); + AThermal_releaseManager(self->thermalManager); + self->thermalManager = nullptr; + } + } + + if (s_singleton == self) + s_singleton = nullptr; + + G_OBJECT_CLASS(wpe_android_power_profile_monitor_parent_class)->dispose(object); +} + +static void wpe_android_power_profile_monitor_class_init(WPEAndroidPowerMonitorClass* klass) +{ + GObjectClass* objectClass = G_OBJECT_CLASS(klass); + objectClass->dispose = wpeAndroidPowerProfileMonitorDispose; + objectClass->get_property = wpeAndroidPowerProfileMonitorGetProperty; + + g_object_class_override_property(objectClass, PROP_POWER_SAVER_ENABLED, "power-saver-enabled"); +} + +void wpe_android_power_profile_monitor_set_battery_saver(gboolean isPowerSaveMode) +{ + if (s_singleton == nullptr) { + Logging::logDebug("WPEAndroidPowerProfileMonitor::set_battery_saver called before init, ignoring"); + return; + } + + bool newValue = (isPowerSaveMode != FALSE); + if (s_singleton->isBatterySaverActive.load() != newValue) { + Logging::logDebug("WPEAndroidPowerProfileMonitor: Battery saver changed to %s", newValue ? "true" : "false"); + s_singleton->isBatterySaverActive.store(newValue); + g_main_context_invoke( + nullptr, + +[](gpointer userData) -> gboolean { + updatePowerStateOnMainThread(WPE_ANDROID_POWER_PROFILE_MONITOR(userData)); + return G_SOURCE_REMOVE; + }, + s_singleton); + } +} + +void wpe_android_power_profile_monitor_set_thermal_throttling(gboolean isThermalThrottling) +{ + if (s_singleton == nullptr) { + Logging::logDebug("WPEAndroidPowerProfileMonitor::set_thermal_throttling called before init, ignoring"); + return; + } + + bool newValue = (isThermalThrottling != FALSE); + if (s_singleton->isThermalThrottling.load() != newValue) { + Logging::logDebug( + "WPEAndroidPowerProfileMonitor: Thermal throttling changed to %s", newValue ? "true" : "false"); + s_singleton->isThermalThrottling.store(newValue); + g_main_context_invoke( + nullptr, + +[](gpointer userData) -> gboolean { + updatePowerStateOnMainThread(WPE_ANDROID_POWER_PROFILE_MONITOR(userData)); + return G_SOURCE_REMOVE; + }, + s_singleton); + } +} + +void WPEAndroidPowerProfileMonitor::configureJNIMappings() +{ + Logging::logDebug("WPEAndroidPowerProfileMonitor::configureJNIMappings()"); + getJNIWPEPowerMonitorCache(); +} + +void WPEAndroidPowerProfileMonitor::registerExtension() +{ + Logging::logDebug("WPEAndroidPowerProfileMonitor::registerExtension()"); + + g_type_ensure(WPE_TYPE_ANDROID_POWER_PROFILE_MONITOR); + + GIOExtensionPoint* extensionPoint = g_io_extension_point_lookup(G_POWER_PROFILE_MONITOR_EXTENSION_POINT_NAME); + if (extensionPoint == nullptr) + extensionPoint = g_io_extension_point_register(G_POWER_PROFILE_MONITOR_EXTENSION_POINT_NAME); + + g_io_extension_point_implement( + G_POWER_PROFILE_MONITOR_EXTENSION_POINT_NAME, WPE_TYPE_ANDROID_POWER_PROFILE_MONITOR, "android", 10); + + Logging::logDebug("WPEAndroidPowerProfileMonitor: Registered as GPowerProfileMonitor extension"); +} diff --git a/wpeview/src/main/cpp/Runtime/WPEAndroidPowerProfileMonitor.h b/wpeview/src/main/cpp/Runtime/WPEAndroidPowerProfileMonitor.h new file mode 100644 index 000000000..69a3972e4 --- /dev/null +++ b/wpeview/src/main/cpp/Runtime/WPEAndroidPowerProfileMonitor.h @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2025 maceip + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define WPE_TYPE_ANDROID_POWER_PROFILE_MONITOR (wpe_android_power_profile_monitor_get_type()) +G_DECLARE_FINAL_TYPE( + WPEAndroidPowerMonitor, wpe_android_power_profile_monitor, WPE, ANDROID_POWER_PROFILE_MONITOR, GObject) + +/** + * Updates the battery saver state from Java layer. + * Called via JNI when the battery saver mode changes. + */ +void wpe_android_power_profile_monitor_set_battery_saver(gboolean isPowerSaveMode); + +/** + * Updates the thermal throttling state. + * Can be called from native code, though thermal status is typically + * monitored internally via AThermalManager. + */ +void wpe_android_power_profile_monitor_set_thermal_throttling(gboolean isThermalThrottling); + +G_END_DECLS + +/** + * WPEAndroidPowerProfileMonitor implements GPowerProfileMonitor for Android. + * + * Aggregates two signals to determine "Low Power Mode": + * 1. Thermal Status (via NDK AThermalManager, API 30+) + * 2. Battery Saver Mode (via Java PowerManager broadcast) + * + * When either condition indicates power constraints, WebKit's LowPowerModeNotifierGLib + * sees "power-saver-enabled" as TRUE, causing reduced timer precision, stopped + * smooth animations, and throttled background tabs. + */ +class WPEAndroidPowerProfileMonitor { +public: + static void configureJNIMappings(); + static void registerExtension(); +}; diff --git a/wpeview/src/main/java/org/wpewebkit/wpe/WKRuntime.java b/wpeview/src/main/java/org/wpewebkit/wpe/WKRuntime.java index 3c2289562..93a0d7d5f 100644 --- a/wpeview/src/main/java/org/wpewebkit/wpe/WKRuntime.java +++ b/wpeview/src/main/java/org/wpewebkit/wpe/WKRuntime.java @@ -67,6 +67,7 @@ public final class WKRuntime { public @Nullable Context getApplicationContext() { return applicationContext; } private LooperHelperThread looperHelperThread = null; + private WPEPowerMonitor powerMonitor = null; public static void enableRemoteInspector(int inspectorPort, boolean useHttpInspector) { WKRuntime.inspectorPort = inspectorPort; @@ -111,12 +112,20 @@ public void initialize(@NonNull Context context) { setupNativeEnvironment(envStrings.toArray(new String[envStrings.size()])); nativeInit(); looperHelperThread = new LooperHelperThread(); + + // Start power monitor for battery saver and thermal state tracking + powerMonitor = new WPEPowerMonitor(applicationContext); + powerMonitor.start(); } } @Override protected void finalize() throws Throwable { super.finalize(); + if (powerMonitor != null) { + powerMonitor.stop(); + powerMonitor = null; + } nativeShut(); } diff --git a/wpeview/src/main/java/org/wpewebkit/wpe/WPEPowerMonitor.java b/wpeview/src/main/java/org/wpewebkit/wpe/WPEPowerMonitor.java new file mode 100644 index 000000000..b72d29c28 --- /dev/null +++ b/wpeview/src/main/java/org/wpewebkit/wpe/WPEPowerMonitor.java @@ -0,0 +1,112 @@ +/** + * Copyright (C) 2025 maceip + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.wpewebkit.wpe; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.PowerManager; +import android.util.Log; + +import androidx.annotation.NonNull; + +/** + * Monitors Battery Saver mode and notifies the native WebKit layer via JNI. + * Combined with thermal throttling from NDK AThermalManager, this enables WebKit + * to reduce resource usage (animations, timer precision, background tabs) when needed. + */ +public final class WPEPowerMonitor { + private static final String LOGTAG = "WPEPowerMonitor"; + + private static native void nativeOnPowerSaveModeChanged(boolean isPowerSaveMode); + + private final Context mContext; + private final BroadcastReceiver mReceiver; + private boolean mIsRegistered = false; + + public WPEPowerMonitor(@NonNull Context context) { + mContext = context.getApplicationContext(); + mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(intent.getAction())) { + notifyState(); + } + } + }; + } + + public void start() { + if (mIsRegistered) { + Log.d(LOGTAG, "Power monitor already started"); + return; + } + + Log.d(LOGTAG, "Starting power monitor"); + + IntentFilter filter = new IntentFilter(); + filter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mContext.registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + mContext.registerReceiver(mReceiver, filter); + } + + mIsRegistered = true; + notifyState(); + } + + public void stop() { + if (!mIsRegistered) { + Log.d(LOGTAG, "Power monitor already stopped"); + return; + } + + Log.d(LOGTAG, "Stopping power monitor"); + + try { + mContext.unregisterReceiver(mReceiver); + } catch (IllegalArgumentException e) { + Log.w(LOGTAG, "Receiver was not registered: " + e.getMessage()); + } + + mIsRegistered = false; + } + + private void notifyState() { + PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + boolean isPowerSave = (pm != null) && pm.isPowerSaveMode(); + + Log.d(LOGTAG, "Power save mode: " + isPowerSave); + + try { + nativeOnPowerSaveModeChanged(isPowerSave); + } catch (UnsatisfiedLinkError e) { + Log.e(LOGTAG, "Native method not available: " + e.getMessage()); + } + } + + public boolean isPowerSaveMode() { + PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + return (pm != null) && pm.isPowerSaveMode(); + } +}