diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b846e26..6372718 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
Unit,
val setBatchSize: (Int) -> Unit,
val setScreenOffRecordEnabled: (Boolean) -> Unit,
+ val setPreciseScreenOffRecordEnabled: (Boolean) -> Unit,
val setAlwaysPollingScreenStatusEnabled: (Boolean) -> Unit,
val setSegmentDurationMin: (Long) -> Unit,
val setRootBootAutoStartEnabled: (Boolean) -> Unit
diff --git a/app/src/main/java/yangfentuozi/batteryrecorder/ui/model/SettingsUiState.kt b/app/src/main/java/yangfentuozi/batteryrecorder/ui/model/SettingsUiState.kt
index e765bea..b9ef740 100644
--- a/app/src/main/java/yangfentuozi/batteryrecorder/ui/model/SettingsUiState.kt
+++ b/app/src/main/java/yangfentuozi/batteryrecorder/ui/model/SettingsUiState.kt
@@ -49,6 +49,8 @@ data class SettingsUiState(
val batchSize: Int = ServerSettings().batchSize,
/** 息屏记录 */
val recordScreenOffEnabled: Boolean = ServerSettings().screenOffRecordEnabled,
+ /** 精确息屏记录 */
+ val preciseScreenOffRecordEnabled: Boolean = ServerSettings().preciseScreenOffRecordEnabled,
/** 轮询获取息屏状态 */
val alwaysPollingScreenStatusEnabled: Boolean = ServerSettings().alwaysPollingScreenStatusEnabled,
/** 自动分段时间 */
diff --git a/app/src/main/java/yangfentuozi/batteryrecorder/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/yangfentuozi/batteryrecorder/ui/screens/settings/SettingsScreen.kt
index 29f369f..c320236 100644
--- a/app/src/main/java/yangfentuozi/batteryrecorder/ui/screens/settings/SettingsScreen.kt
+++ b/app/src/main/java/yangfentuozi/batteryrecorder/ui/screens/settings/SettingsScreen.kt
@@ -67,6 +67,7 @@ fun SettingsScreen(
setWriteLatencyMs = settingsViewModel::setWriteLatencyMs,
setBatchSize = settingsViewModel::setBatchSize,
setScreenOffRecordEnabled = settingsViewModel::setScreenOffRecordEnabled,
+ setPreciseScreenOffRecordEnabled = settingsViewModel::setPreciseScreenOffRecordEnabled,
setAlwaysPollingScreenStatusEnabled = settingsViewModel::setAlwaysPollingScreenStatusEnabled,
setSegmentDurationMin = settingsViewModel::setSegmentDurationMin,
setRootBootAutoStartEnabled = settingsViewModel::setRootBootAutoStartEnabled
@@ -101,6 +102,7 @@ fun SettingsScreen(
writeLatencyMs = serverSettings.writeLatencyMs,
batchSize = serverSettings.batchSize,
recordScreenOffEnabled = serverSettings.screenOffRecordEnabled,
+ preciseScreenOffRecordEnabled = serverSettings.preciseScreenOffRecordEnabled,
alwaysPollingScreenStatusEnabled = serverSettings.alwaysPollingScreenStatusEnabled,
segmentDurationMin = serverSettings.segmentDurationMin,
rootBootAutoStartEnabled = appSettings.rootBootAutoStartEnabled,
diff --git a/app/src/main/java/yangfentuozi/batteryrecorder/ui/viewmodel/SettingsViewModel.kt b/app/src/main/java/yangfentuozi/batteryrecorder/ui/viewmodel/SettingsViewModel.kt
index dbef6d6..74b953b 100644
--- a/app/src/main/java/yangfentuozi/batteryrecorder/ui/viewmodel/SettingsViewModel.kt
+++ b/app/src/main/java/yangfentuozi/batteryrecorder/ui/viewmodel/SettingsViewModel.kt
@@ -122,7 +122,7 @@ class SettingsViewModel : ViewModel() {
LoggerX.d(
TAG,
- "[设置] loadSettings 完成: notification=${currentServerSettings.notificationEnabled} compatMode=${currentServerSettings.notificationCompatModeEnabled} dualCell=${currentServerSettings.dualCellEnabled} calibration=${currentServerSettings.calibrationValue} intervalMs=${currentServerSettings.recordIntervalMs} writeLatencyMs=${currentServerSettings.writeLatencyMs} batchSize=${currentServerSettings.batchSize} screenOffRecord=${currentServerSettings.screenOffRecordEnabled} polling=${currentServerSettings.alwaysPollingScreenStatusEnabled} logLevel=${currentServerSettings.logLevel}"
+ "[设置] loadSettings 完成: notification=${currentServerSettings.notificationEnabled} compatMode=${currentServerSettings.notificationCompatModeEnabled} dualCell=${currentServerSettings.dualCellEnabled} calibration=${currentServerSettings.calibrationValue} intervalMs=${currentServerSettings.recordIntervalMs} writeLatencyMs=${currentServerSettings.writeLatencyMs} batchSize=${currentServerSettings.batchSize} screenOffRecord=${currentServerSettings.screenOffRecordEnabled} preciseScreenOffRecord=${currentServerSettings.preciseScreenOffRecordEnabled} polling=${currentServerSettings.alwaysPollingScreenStatusEnabled} logLevel=${currentServerSettings.logLevel}"
)
}
@@ -236,6 +236,20 @@ class SettingsViewModel : ViewModel() {
}
}
+ /**
+ * 更新精确息屏记录开关并下发到运行中的服务端。
+ *
+ * @param enabled `true` 表示允许 Server 在息屏记录阶段持有唤醒锁;`false` 表示恢复默认自然息屏采样。
+ * @return 无。
+ */
+ fun setPreciseScreenOffRecordEnabled(enabled: Boolean) {
+ updateServerSettings(
+ message = "[设置] 更新精确息屏记录并准备下发: enabled=$enabled"
+ ) { current ->
+ current.copy(preciseScreenOffRecordEnabled = enabled)
+ }
+ }
+
fun setAlwaysPollingScreenStatusEnabled(enabled: Boolean) {
updateServerSettings(
message = "[设置] 更新轮询亮屏状态并准备下发: enabled=$enabled"
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 8930efb..f257e48 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -95,6 +95,8 @@
实时功率通知
通知兼容模式
息屏记录
+ 精确息屏记录
+ 可能导致息屏耗电增高
轮询获取息屏状态
采样间隔
写入延迟
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d2ce007..d248f2d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -95,6 +95,8 @@
Realtime power notification
Notification compatibility mode
Record when screen is off
+ Precise screen-off recording
+ May increase screen-off power draw.
Poll screen state
Sampling interval
Write latency
diff --git a/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt b/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt
index be8fcf6..2d540d0 100644
--- a/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt
+++ b/server/src/main/java/yangfentuozi/batteryrecorder/server/Server.kt
@@ -82,31 +82,43 @@ class Server internal constructor() : IService.Stub() {
monitor.unregisterRecordListener(listener)
}
- override fun updateConfig(settings: ServerSettings) {
- Handlers.common.post {
- LoggerX.d(
- tag,
- "updateConfig: 应用配置, notification=${settings.notificationEnabled} compatMode=${settings.notificationCompatModeEnabled} dualCell=${settings.dualCellEnabled} calibration=${settings.calibrationValue} intervalMs=${settings.recordIntervalMs} writeLatencyMs=${settings.writeLatencyMs} batchSize=${settings.batchSize} screenOffRecord=${settings.screenOffRecordEnabled} segmentDurationMin=${settings.segmentDurationMin} logLevel=${settings.logLevel} polling=${settings.alwaysPollingScreenStatusEnabled}"
- )
- LoggerX.maxHistoryDays = settings.maxHistoryDays
- LoggerX.logLevel = settings.logLevel
+ /**
+ * 应用一份服务端配置到当前运行时实例。
+ *
+ * @param settings 要生效的服务端配置。
+ * @param source 本次配置应用来源,仅用于日志定位。
+ * @return 无。
+ */
+ private fun applyConfigInternal(settings: ServerSettings, source: String) {
+ LoggerX.d(
+ tag,
+ "$source: 应用配置, notification=${settings.notificationEnabled} compatMode=${settings.notificationCompatModeEnabled} dualCell=${settings.dualCellEnabled} calibration=${settings.calibrationValue} intervalMs=${settings.recordIntervalMs} writeLatencyMs=${settings.writeLatencyMs} batchSize=${settings.batchSize} screenOffRecord=${settings.screenOffRecordEnabled} preciseScreenOffRecord=${settings.preciseScreenOffRecordEnabled} segmentDurationMin=${settings.segmentDurationMin} logLevel=${settings.logLevel} polling=${settings.alwaysPollingScreenStatusEnabled}"
+ )
+ LoggerX.maxHistoryDays = settings.maxHistoryDays
+ LoggerX.logLevel = settings.logLevel
- unlockOPlusSampleTimeLimit(settings.recordIntervalMs.coerceAtLeast(200))
+ unlockOPlusSampleTimeLimit(settings.recordIntervalMs.coerceAtLeast(200))
- monitor.notificationPowerMultiplier = computeNotificationPowerMultiplier(
- dualCellEnabled = settings.dualCellEnabled,
- calibrationValue = settings.calibrationValue,
- )
- monitor.setNotificationCompatModeEnabled(settings.notificationCompatModeEnabled)
- monitor.setNotificationEnabled(settings.notificationEnabled)
- monitor.alwaysPollingScreenStatusEnabled = settings.alwaysPollingScreenStatusEnabled
- monitor.recordIntervalMs = settings.recordIntervalMs
- monitor.screenOffRecord = settings.screenOffRecordEnabled
- monitor.notifyLock()
-
- writer.flushIntervalMs = settings.writeLatencyMs
- writer.batchSize = settings.batchSize
- writer.maxSegmentDurationMs = settings.segmentDurationMin * 60 * 1000L
+ monitor.notificationPowerMultiplier = computeNotificationPowerMultiplier(
+ dualCellEnabled = settings.dualCellEnabled,
+ calibrationValue = settings.calibrationValue,
+ )
+ monitor.setNotificationCompatModeEnabled(settings.notificationCompatModeEnabled)
+ monitor.setNotificationEnabled(settings.notificationEnabled)
+ monitor.alwaysPollingScreenStatusEnabled = settings.alwaysPollingScreenStatusEnabled
+ monitor.recordIntervalMs = settings.recordIntervalMs
+ monitor.screenOffRecord = settings.screenOffRecordEnabled
+ monitor.preciseScreenOffRecordEnabled = settings.preciseScreenOffRecordEnabled
+ monitor.notifyLock()
+
+ writer.flushIntervalMs = settings.writeLatencyMs
+ writer.batchSize = settings.batchSize
+ writer.maxSegmentDurationMs = settings.segmentDurationMin * 60 * 1000L
+ }
+
+ override fun updateConfig(settings: ServerSettings) {
+ Handlers.common.post {
+ applyConfigInternal(settings, "updateConfig")
}
}
@@ -473,7 +485,8 @@ class Server internal constructor() : IService.Stub() {
LoggerX.i(tag, "init: 通过 ConfigProvider 读取配置")
ConfigUtil.getServerSettingsByContentProvider()
}
- serverSettings?.let(::updateConfig) ?: LoggerX.w(tag, "init: 未读取到配置, 使用当前默认值")
+ serverSettings?.let { applyConfigInternal(it, "init") }
+ ?: LoggerX.w(tag, "init: 未读取到配置, 使用当前默认值")
monitor.start()
LoggerX.i(tag, "init: Monitor 已启动, 进入消息循环")
diff --git a/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt b/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt
index ebf507f..2db8e1c 100644
--- a/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt
+++ b/server/src/main/java/yangfentuozi/batteryrecorder/server/recorder/Monitor.kt
@@ -9,11 +9,13 @@ import android.hardware.display.IDisplayManager
import android.hardware.display.IDisplayManagerCallback
import android.os.Handler
import android.os.IPowerManager
+import android.os.PowerManager
import android.os.RemoteCallbackList
import android.os.RemoteException
import android.os.ServiceManager
import android.system.Os
import androidx.annotation.Keep
+import yangfentuozi.batteryrecorder.server.fakecontext.FakeContext
import yangfentuozi.batteryrecorder.server.notification.LocalNotificationUtil
import yangfentuozi.batteryrecorder.server.notification.NotificationInfo
import yangfentuozi.batteryrecorder.server.notification.NotificationUtil
@@ -68,6 +70,26 @@ class Monitor(
@Volatile
var screenOffRecord: Boolean = SettingsConstants.screenOffRecordEnabled.def
+ set(value) {
+ val oldValue = field
+ if (oldValue == value) return
+ field = value
+ LoggerX.d(tag, "screenOffRecord: 配置变更, $oldValue -> $value")
+ updatePreciseScreenOffWakeLockState()
+ }
+
+ @Volatile
+ var preciseScreenOffRecordEnabled: Boolean = SettingsConstants.preciseScreenOffRecordEnabled.def
+ set(value) {
+ val oldValue = field
+ if (oldValue == value) return
+ field = value
+ LoggerX.d(tag, "preciseScreenOffRecordEnabled: 配置变更, $oldValue -> $value")
+ if (!oldValue && value) {
+ preparePreciseScreenOffWakeLock()
+ }
+ updatePreciseScreenOffWakeLockState()
+ }
@Volatile
var notificationUtil: NotificationUtil? = null
@@ -107,6 +129,10 @@ class Monitor(
@Volatile
private var stopped = false
+ private var preciseScreenOffWakeLock: PowerManager.WakeLock? = null
+ @Volatile
+ private var preciseScreenOffWakeLockDisabledForCurrentServer = false
+ private var lastPreciseScreenOffWakeLockDecisionReason: String? = null
private val lock = ReentrantLock()
private val condition = lock.newCondition()
private val callbackHandler: Handler
@@ -127,6 +153,7 @@ class Monitor(
LoggerX.d(tag, "@thread: 亮屏状态变化, $oldIsInteractive -> $latestIsInteractive")
}
isInteractive = latestIsInteractive
+ updatePreciseScreenOffWakeLockState()
}
val record = LineRecord(
timestamp,
@@ -195,6 +222,7 @@ class Monitor(
while (!stopped && !screenOffRecord && !isInteractive && alwaysPollingScreenStatusEnabled) {
condition.await(recordIntervalMs, TimeUnit.MILLISECONDS)
isInteractive = iPowerManager.isInteractive
+ updatePreciseScreenOffWakeLockState()
}
} else {
LoggerX.d(tag, "@thread: 暂停采样, 等待亮屏事件")
@@ -208,8 +236,11 @@ class Monitor(
fun start() {
LoggerX.d(tag,
- "start: alwaysPollingScreenStatusEnabled=$alwaysPollingScreenStatusEnabled screenOffRecord=$screenOffRecord"
+ "start: alwaysPollingScreenStatusEnabled=$alwaysPollingScreenStatusEnabled screenOffRecord=$screenOffRecord preciseScreenOffRecordEnabled=$preciseScreenOffRecordEnabled"
)
+ if (preciseScreenOffRecordEnabled) {
+ preparePreciseScreenOffWakeLock()
+ }
try {
iActivityTaskManager.registerTaskStackListener(taskStackListener)
if (!alwaysPollingScreenStatusEnabled) {
@@ -226,10 +257,24 @@ class Monitor(
throw RuntimeException("start: 获取当前焦点任务信息失败", e)
}
isInteractive = iPowerManager.isInteractive
+ updatePreciseScreenOffWakeLockState()
LoggerX.d(tag, "start: initial isInteractive=$isInteractive")
thread.start()
}
+ /**
+ * 在非 Binder 回调线程预创建精确息屏记录的唤醒锁实例,避免首次息屏回调落在 Binder 线程时
+ * 触发 `FakeContext.systemContext` 的惰性初始化,进而命中无 Looper 线程创建 ActivityThread 的崩溃。
+ *
+ * @return 无;预创建失败时会在当前 Server 生命周期内静默禁用该功能。
+ */
+ private fun preparePreciseScreenOffWakeLock() {
+ val wakeLock = requirePreciseScreenOffWakeLock()
+ if (wakeLock != null) {
+ LoggerX.d(tag, "preparePreciseScreenOffWakeLock: 唤醒锁实例预创建完成")
+ }
+ }
+
private fun registerDisplayEventCallback() {
if (displayCallbackRegistered) {
LoggerX.v(tag, "registerDisplayEventCallback: 已注册, skip")
@@ -246,6 +291,7 @@ class Monitor(
val oldIsInteractive = isInteractive
val latestIsInteractive = iPowerManager.isInteractive
isInteractive = latestIsInteractive
+ updatePreciseScreenOffWakeLockState()
LoggerX.d(tag,
"onDisplayEvent: displayId=$displayId event=$event interactive $oldIsInteractive -> $latestIsInteractive paused=$paused"
)
@@ -271,6 +317,7 @@ class Monitor(
fun stop() {
stopped = true
notifyLock()
+ releasePreciseScreenOffWakeLockIfHeld("服务停止")
try {
iActivityTaskManager.unregisterTaskStackListener(taskStackListener)
} catch (e: RemoteException) {
@@ -355,8 +402,141 @@ class Monitor(
}
}
+ /**
+ * 按当前屏幕状态与配置同步精确息屏记录的唤醒锁状态。
+ *
+ * 这里只在“开启精确息屏记录 + 开启息屏记录 + 当前确实处于息屏”时持锁,
+ * 避免把亮屏阶段也变成常驻保活,额外抬高非目标场景功耗。
+ *
+ * @return 无;状态不满足时会主动释放已持有的唤醒锁。
+ */
+ @Synchronized
+ private fun updatePreciseScreenOffWakeLockState() {
+ val shouldHoldWakeLock =
+ preciseScreenOffRecordEnabled &&
+ !preciseScreenOffWakeLockDisabledForCurrentServer &&
+ screenOffRecord &&
+ !isInteractive &&
+ !stopped
+ val currentReason = when {
+ stopped -> "服务停止"
+ preciseScreenOffWakeLockDisabledForCurrentServer -> "当前服务已禁用精确息屏记录"
+ isInteractive -> "屏幕亮起"
+ !screenOffRecord -> "息屏记录关闭"
+ !preciseScreenOffRecordEnabled -> "精确息屏记录关闭"
+ else -> "满足持锁条件"
+ }
+
+ val wakeLock = preciseScreenOffWakeLock
+ if (shouldHoldWakeLock) {
+ if (wakeLock == null) {
+ LoggerX.e(tag, "updatePreciseScreenOffWakeLockState: 满足持锁条件但唤醒锁尚未初始化")
+ lastPreciseScreenOffWakeLockDecisionReason = "唤醒锁未初始化"
+ return
+ }
+ if (!wakeLock.isHeld) {
+ LoggerX.d(
+ tag,
+ "updatePreciseScreenOffWakeLockState: 准备持有唤醒锁, preciseScreenOffRecordEnabled=$preciseScreenOffRecordEnabled screenOffRecord=$screenOffRecord isInteractive=$isInteractive stopped=$stopped"
+ )
+ wakeLock.acquire()
+ if (wakeLock.isHeld) {
+ LoggerX.i(tag, "updatePreciseScreenOffWakeLockState: 已持有唤醒锁")
+ } else {
+ LoggerX.w(tag, "updatePreciseScreenOffWakeLockState: acquire() 返回后仍未持有唤醒锁")
+ }
+ } else {
+ LoggerX.d(tag, "updatePreciseScreenOffWakeLockState: 唤醒锁已处于持有状态,跳过重复 acquire")
+ }
+ lastPreciseScreenOffWakeLockDecisionReason = currentReason
+ return
+ }
+
+ if (wakeLock?.isHeld == true) {
+ releasePreciseScreenOffWakeLockIfHeld(currentReason)
+ lastPreciseScreenOffWakeLockDecisionReason = currentReason
+ return
+ }
+ if (lastPreciseScreenOffWakeLockDecisionReason != currentReason) {
+ LoggerX.d(
+ tag,
+ "updatePreciseScreenOffWakeLockState: 当前不持有唤醒锁, reason=$currentReason preciseScreenOffRecordEnabled=$preciseScreenOffRecordEnabled screenOffRecord=$screenOffRecord isInteractive=$isInteractive stopped=$stopped"
+ )
+ lastPreciseScreenOffWakeLockDecisionReason = currentReason
+ }
+ }
+
+ /**
+ * 释放当前已持有的精确息屏记录唤醒锁。
+ *
+ * @param reason 本次释放的直接原因,用于日志定位。
+ * @return 无;若尚未创建或当前未持锁则直接返回。
+ */
+ @Synchronized
+ private fun releasePreciseScreenOffWakeLockIfHeld(reason: String) {
+ val wakeLock = preciseScreenOffWakeLock ?: return
+ if (!wakeLock.isHeld) return
+ LoggerX.d(tag, "releasePreciseScreenOffWakeLockIfHeld: 准备释放唤醒锁, reason=$reason")
+ wakeLock.release()
+ if (!wakeLock.isHeld) {
+ LoggerX.i(tag, "releasePreciseScreenOffWakeLockIfHeld: 已释放唤醒锁, reason=$reason")
+ } else {
+ LoggerX.w(tag, "releasePreciseScreenOffWakeLockIfHeld: release() 返回后仍处于持有状态, reason=$reason")
+ }
+ }
+
+ /**
+ * 获取精确息屏记录使用的部分唤醒锁实例。
+ *
+ * 初始化失败时仅记录错误,并在当前 Server 生命周期内静默禁用该功能,避免在回调线程反复抛异常。
+ *
+ * @return 返回单例 `PARTIAL_WAKE_LOCK`;当前服务已禁用该能力时返回 `null`。
+ */
+ private fun requirePreciseScreenOffWakeLock(): PowerManager.WakeLock? {
+ if (preciseScreenOffWakeLockDisabledForCurrentServer) {
+ LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 当前服务已禁用精确息屏记录,跳过唤醒锁初始化")
+ return null
+ }
+ preciseScreenOffWakeLock?.let {
+ LoggerX.d(
+ tag,
+ "requirePreciseScreenOffWakeLock: 复用已有唤醒锁实例, held=${it.isHeld}"
+ )
+ return it
+ }
+ try {
+ LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 开始创建唤醒锁实例")
+ LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 开始获取 systemContext")
+ val systemContext = FakeContext.systemContext
+ LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 获取 systemContext 成功, context=${systemContext.javaClass.name}")
+ LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 开始获取 PowerManager")
+ val powerManager = systemContext.getSystemService(PowerManager::class.java)
+ ?: throw IllegalStateException("获取 PowerManager 失败")
+ LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 获取 PowerManager 成功, service=${powerManager.javaClass.name}")
+ LoggerX.d(tag, "requirePreciseScreenOffWakeLock: 开始调用 newWakeLock, tag=$PRECISE_SCREEN_OFF_WAKE_LOCK_TAG")
+ return powerManager.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK,
+ PRECISE_SCREEN_OFF_WAKE_LOCK_TAG
+ ).apply {
+ setReferenceCounted(false)
+ preciseScreenOffWakeLock = this
+ LoggerX.d(
+ tag,
+ "requirePreciseScreenOffWakeLock: 唤醒锁实例创建完成, tag=$PRECISE_SCREEN_OFF_WAKE_LOCK_TAG held=$isHeld"
+ )
+ }
+ } catch (t: Throwable) {
+ LoggerX.e(tag, "requirePreciseScreenOffWakeLock: 创建唤醒锁失败", tr = t)
+ preciseScreenOffWakeLockDisabledForCurrentServer = true
+ preciseScreenOffWakeLock = null
+ LoggerX.w(tag, "requirePreciseScreenOffWakeLock: 当前服务已静默禁用精确息屏记录")
+ return null
+ }
+ }
+
companion object {
private const val POWER_SCALE_DIVISOR = 1_000_000_000_000.0
+ private const val PRECISE_SCREEN_OFF_WAKE_LOCK_TAG = "BatteryRecorder:PreciseScreenOffRecord"
fun computeNotificationPowerMultiplier(
dualCellEnabled: Boolean,
diff --git a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ConfigUtil.kt b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ConfigUtil.kt
index 1a1d54d..eff70f2 100644
--- a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ConfigUtil.kt
+++ b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ConfigUtil.kt
@@ -116,7 +116,7 @@ object ConfigUtil {
private fun logServerSettings(source: String, settings: ServerSettings) {
LoggerX.d(
TAG,
- "$source: notification=${settings.notificationEnabled} compatMode=${settings.notificationCompatModeEnabled} dualCell=${settings.dualCellEnabled} calibration=${settings.calibrationValue} intervalMs=${settings.recordIntervalMs} batchSize=${settings.batchSize} writeLatencyMs=${settings.writeLatencyMs} screenOffRecord=${settings.screenOffRecordEnabled} polling=${settings.alwaysPollingScreenStatusEnabled} logLevel=${settings.logLevel}"
+ "$source: notification=${settings.notificationEnabled} compatMode=${settings.notificationCompatModeEnabled} dualCell=${settings.dualCellEnabled} calibration=${settings.calibrationValue} intervalMs=${settings.recordIntervalMs} batchSize=${settings.batchSize} writeLatencyMs=${settings.writeLatencyMs} screenOffRecord=${settings.screenOffRecordEnabled} preciseScreenOffRecord=${settings.preciseScreenOffRecordEnabled} polling=${settings.alwaysPollingScreenStatusEnabled} logLevel=${settings.logLevel}"
)
}
}
diff --git a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ServerSettingsCodec.kt b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ServerSettingsCodec.kt
index 58a416f..dc1bf6d 100644
--- a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ServerSettingsCodec.kt
+++ b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/ServerSettingsCodec.kt
@@ -85,6 +85,10 @@ object ServerSettingsCodec {
SettingsConstants.screenOffRecordEnabled.key,
settings.screenOffRecordEnabled
)
+ editor.putBoolean(
+ SettingsConstants.preciseScreenOffRecordEnabled.key,
+ settings.preciseScreenOffRecordEnabled
+ )
editor.putLong(SettingsConstants.segmentDurationMin.key, settings.segmentDurationMin)
editor.putLong(SettingsConstants.logMaxHistoryDays.key, settings.maxHistoryDays)
editor.putInt(
@@ -123,6 +127,9 @@ object ServerSettingsCodec {
screenOffRecordEnabled =
source.boolean(SettingsConstants.screenOffRecordEnabled.key)
?: SettingsConstants.screenOffRecordEnabled.def,
+ preciseScreenOffRecordEnabled =
+ source.boolean(SettingsConstants.preciseScreenOffRecordEnabled.key)
+ ?: SettingsConstants.preciseScreenOffRecordEnabled.def,
segmentDurationMin =
source.long(SettingsConstants.segmentDurationMin.key)
?: SettingsConstants.segmentDurationMin.def,
diff --git a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/SettingsConstants.kt b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/SettingsConstants.kt
index a36ecb4..b47a471 100644
--- a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/SettingsConstants.kt
+++ b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/SettingsConstants.kt
@@ -87,6 +87,13 @@ object SettingsConstants {
def = true
)
+ /** 是否启用精确息屏记录;开启后 Server 会在息屏记录阶段持有唤醒锁。 */
+ val preciseScreenOffRecordEnabled =
+ BooleanConfigItem(
+ key = "precise_screen_off_record_enabled",
+ def = false
+ )
+
/** 数据分段时长(分钟) */
val segmentDurationMin =
LongConfigItem(
diff --git a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/dataclass/ServerSettings.kt b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/dataclass/ServerSettings.kt
index b924fc6..aab132b 100644
--- a/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/dataclass/ServerSettings.kt
+++ b/shared/src/main/java/yangfentuozi/batteryrecorder/shared/config/dataclass/ServerSettings.kt
@@ -33,6 +33,9 @@ data class ServerSettings(
val writeLatencyMs: Long = SettingsConstants.writeLatencyMs.def,
/** 息屏后是否继续采样与记录。 */
val screenOffRecordEnabled: Boolean = SettingsConstants.screenOffRecordEnabled.def,
+ /** 是否在息屏记录阶段持有唤醒锁,提升采样定时精度。 */
+ val preciseScreenOffRecordEnabled: Boolean =
+ SettingsConstants.preciseScreenOffRecordEnabled.def,
/** 单个记录文件按时间自动分段的时长,单位分钟,0 表示不分段。 */
val segmentDurationMin: Long = SettingsConstants.segmentDurationMin.def,
/** 日志文件保留天数。 */