From c915bfbf86cc422ba6bb3dadc3f27a39499a7153 Mon Sep 17 00:00:00 2001 From: Amirhossein Akhlaghpour Date: Sun, 26 Apr 2026 17:38:28 +0330 Subject: [PATCH 1/4] fix(android): reduce Connected & Screen messages when host screen is off Signed-off-by: Amirhossein Akhlaghpour --- .../com/carriez/flutter_hbb/InputService.kt | 4 ++ .../com/carriez/flutter_hbb/MainService.kt | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt index 3ca83fbac..cceb2ab3d 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt @@ -91,6 +91,10 @@ class InputService : AccessibilityService() { private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) } + fun wakeUpDevice() { + performGlobalAction(GLOBAL_ACTION_HOME) + } + @RequiresApi(Build.VERSION_CODES.N) fun onMouseInput(mask: Int, _x: Int, _y: Int) { val x = max(0, _x) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index 7bb16a00a..6e2800b41 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -62,6 +62,8 @@ const val VIDEO_KEY_FRAME_RATE = 30 class MainService : Service() { + private val wakeRetryHandler = Handler(Looper.getMainLooper()) + @Keep @RequiresApi(Build.VERSION_CODES.N) fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) { @@ -128,6 +130,7 @@ class MainService : Service() { } if (authorized) { if (!isFileTransfer && !isStart) { + wakeAndRefreshIncomingScreenIfNeeded("authorized_connection") startCapture() } onClientAuthorizedNotification(id, type, username, peerId) @@ -228,6 +231,41 @@ class MainService : Service() { private lateinit var notificationChannel: String private lateinit var notificationBuilder: NotificationCompat.Builder + private fun wakeScreen(reason: String) { + if (wakeLock.isHeld) { + Log.d(logTag, "WakeLock release before wake, reason:$reason") + wakeLock.release() + } + Log.d(logTag, "Wake screen, reason:$reason") + wakeLock.acquire(5000) + } + + private fun wakeAndRefreshIncomingScreenIfNeeded(reason: String) { + if (powerManager.isInteractive) { + return + } + wakeRetryHandler.removeCallbacksAndMessages(null) + wakeScreen(reason) + wakeRetryHandler.postDelayed({ + if (powerManager.isInteractive) { + Log.d(logTag, "Skip delayed wake refresh, device already interactive, reason:$reason") + return@postDelayed + } + // HOME works better here than faking a tap. + InputService.ctx?.wakeUpDevice() + FFI.refreshScreen() + }, 500) + wakeRetryHandler.postDelayed({ + if (powerManager.isInteractive) { + Log.d(logTag, "Skip retry wake refresh, device already interactive, reason:$reason") + return@postDelayed + } + wakeScreen("$reason-retry") + InputService.ctx?.wakeUpDevice() + FFI.refreshScreen() + }, 1200) + } + override fun onCreate() { super.onCreate() Log.d(logTag,"MainService onCreate, sdk int:${Build.VERSION.SDK_INT} reuseVirtualDisplay:$reuseVirtualDisplay") @@ -249,6 +287,7 @@ class MainService : Service() { } override fun onDestroy() { + wakeRetryHandler.removeCallbacksAndMessages(null) checkMediaPermission() stopService(Intent(this, FloatingWindowService::class.java)) super.onDestroy() From 3d28e39d09ae814ccf434ea2b0f76e9db7d663b2 Mon Sep 17 00:00:00 2001 From: Amirhossein Akhlaghpour Date: Mon, 27 Apr 2026 21:09:25 +0330 Subject: [PATCH 2/4] fix(android): harden wake/refresh for dozing hosts - throttle wake attempts to reduce abuse/battery drain - poll for interactive + unlocked before HOME/refreshScreen - avoid removeCallbacksAndMessages(null); only cancel wake poll - release wakelock on destroy - rename InputService helper to goHome() --- .../com/carriez/flutter_hbb/InputService.kt | 3 +- .../com/carriez/flutter_hbb/MainService.kt | 112 +++++++++++++----- 2 files changed, 87 insertions(+), 28 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt index cceb2ab3d..bf2a74012 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt @@ -91,7 +91,8 @@ class InputService : AccessibilityService() { private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) } - fun wakeUpDevice() { + // Just HOME. Caller handles the actual wake lock. + internal fun goHome() { performGlobalAction(GLOBAL_ACTION_HOME) } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index 6e2800b41..8a1b2ceb1 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -62,19 +62,32 @@ const val VIDEO_KEY_FRAME_RATE = 30 class MainService : Service() { + companion object { + private const val WAKELOCK_TIMEOUT_MS = 5000L + private const val WAKE_POLL_INTERVAL_MS = 100L + private const val WAKE_MAX_WAIT_MS = 2000L + private const val WAKE_THROTTLE_MS = 10_000L + private const val WAKE_RETRY_AFTER_MS = 1200L + } + private val wakeRetryHandler = Handler(Looper.getMainLooper()) + private val keyguardManager: KeyguardManager by lazy { + applicationContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + } + private var lastWakeAttemptAt = 0L + private var wakePendingReason: String? = null + private var wakeStartedAt = 0L + private var wakeDeadlineAt = 0L + private var wakeDidHome = false + private var wakeRetried = false + private var wakeNotifiedLocked = false @Keep @RequiresApi(Build.VERSION_CODES.N) fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) { // turn on screen with LEFT_DOWN when screen off if (!powerManager.isInteractive && (kind == 0 || mask == LEFT_DOWN)) { - if (wakeLock.isHeld) { - Log.d(logTag, "Turn on Screen, WakeLock release") - wakeLock.release() - } - Log.d(logTag,"Turn on Screen") - wakeLock.acquire(5000) + wakeScreen("pointer_input") } else { when (kind) { 0 -> { // touch @@ -231,39 +244,80 @@ class MainService : Service() { private lateinit var notificationChannel: String private lateinit var notificationBuilder: NotificationCompat.Builder + private val wakePollRunnable = object : Runnable { + override fun run() { + val reason = wakePendingReason ?: return + val now = SystemClock.elapsedRealtime() + if (now >= wakeDeadlineAt) { + Log.w( + logTag, + "wake timeout, interactive=${powerManager.isInteractive}, input=${InputService.isOpen}, reason:$reason" + ) + setTextNotification(null, "Remote session: cannot wake screen") + wakePendingReason = null + return + } + + if (!powerManager.isInteractive) { + if (!wakeRetried && now - wakeStartedAt >= WAKE_RETRY_AFTER_MS) { + wakeRetried = true + wakeScreen("$reason-retry") + } + wakeRetryHandler.postDelayed(this, WAKE_POLL_INTERVAL_MS) + return + } + + if (keyguardManager.isKeyguardLocked) { + if (!wakeNotifiedLocked) { + wakeNotifiedLocked = true + Log.w(logTag, "keyguard locked, skip refresh, reason:$reason") + setTextNotification(null, "Remote session: device locked") + } + wakeRetryHandler.postDelayed(this, WAKE_POLL_INTERVAL_MS) + return + } + + if (!wakeDidHome && InputService.isOpen) { + wakeDidHome = true + InputService.ctx?.goHome() + } else if (!InputService.isOpen) { + Log.w(logTag, "input service not open, skip HOME, reason:$reason") + } + + FFI.refreshScreen() + wakePendingReason = null + } + } + private fun wakeScreen(reason: String) { if (wakeLock.isHeld) { Log.d(logTag, "WakeLock release before wake, reason:$reason") wakeLock.release() } Log.d(logTag, "Wake screen, reason:$reason") - wakeLock.acquire(5000) + wakeLock.acquire(WAKELOCK_TIMEOUT_MS) } private fun wakeAndRefreshIncomingScreenIfNeeded(reason: String) { if (powerManager.isInteractive) { return } - wakeRetryHandler.removeCallbacksAndMessages(null) + val now = SystemClock.elapsedRealtime() + if (now - lastWakeAttemptAt < WAKE_THROTTLE_MS) { + Log.i(logTag, "skip wake due to throttle, reason:$reason") + return + } + lastWakeAttemptAt = now + + wakeRetryHandler.removeCallbacks(wakePollRunnable) + wakePendingReason = reason + wakeStartedAt = now + wakeDeadlineAt = now + WAKE_MAX_WAIT_MS + wakeDidHome = false + wakeRetried = false + wakeNotifiedLocked = false wakeScreen(reason) - wakeRetryHandler.postDelayed({ - if (powerManager.isInteractive) { - Log.d(logTag, "Skip delayed wake refresh, device already interactive, reason:$reason") - return@postDelayed - } - // HOME works better here than faking a tap. - InputService.ctx?.wakeUpDevice() - FFI.refreshScreen() - }, 500) - wakeRetryHandler.postDelayed({ - if (powerManager.isInteractive) { - Log.d(logTag, "Skip retry wake refresh, device already interactive, reason:$reason") - return@postDelayed - } - wakeScreen("$reason-retry") - InputService.ctx?.wakeUpDevice() - FFI.refreshScreen() - }, 1200) + wakeRetryHandler.postDelayed(wakePollRunnable, WAKE_POLL_INTERVAL_MS) } override fun onCreate() { @@ -287,7 +341,11 @@ class MainService : Service() { } override fun onDestroy() { - wakeRetryHandler.removeCallbacksAndMessages(null) + wakeRetryHandler.removeCallbacks(wakePollRunnable) + wakePendingReason = null + if (wakeLock.isHeld) { + wakeLock.release() + } checkMediaPermission() stopService(Intent(this, FloatingWindowService::class.java)) super.onDestroy() From 7115a0912ceb0659e409bba5deff43600cda77b0 Mon Sep 17 00:00:00 2001 From: Amirhossein Akhlaghpour Date: Tue, 28 Apr 2026 00:06:56 +0330 Subject: [PATCH 3/4] fix(android): fix wake polling compile and threading - merge duplicate companion object (Kotlin compile fix) - serialize wake state on main looper - keep lockscreen and accessibility notifications from being overwritten --- .../com/carriez/flutter_hbb/MainService.kt | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index 8a1b2ceb1..1f8e71bf9 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -62,14 +62,6 @@ const val VIDEO_KEY_FRAME_RATE = 30 class MainService : Service() { - companion object { - private const val WAKELOCK_TIMEOUT_MS = 5000L - private const val WAKE_POLL_INTERVAL_MS = 100L - private const val WAKE_MAX_WAIT_MS = 2000L - private const val WAKE_THROTTLE_MS = 10_000L - private const val WAKE_RETRY_AFTER_MS = 1200L - } - private val wakeRetryHandler = Handler(Looper.getMainLooper()) private val keyguardManager: KeyguardManager by lazy { applicationContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager @@ -81,6 +73,7 @@ class MainService : Service() { private var wakeDidHome = false private var wakeRetried = false private var wakeNotifiedLocked = false + private var wakeNotifiedInputDisabled = false @Keep @RequiresApi(Build.VERSION_CODES.N) @@ -211,6 +204,12 @@ class MainService : Service() { private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")} companion object { + private const val WAKELOCK_TIMEOUT_MS = 5000L + private const val WAKE_POLL_INTERVAL_MS = 100L + private const val WAKE_MAX_WAIT_MS = 2000L + private const val WAKE_THROTTLE_MS = 10_000L + private const val WAKE_RETRY_AFTER_MS = 1200L + private var _isReady = false // media permission ready status private var _isStart = false // screen capture start status private var _isAudioStart = false // audio capture start status @@ -249,6 +248,10 @@ class MainService : Service() { val reason = wakePendingReason ?: return val now = SystemClock.elapsedRealtime() if (now >= wakeDeadlineAt) { + if (wakeNotifiedLocked) { + wakePendingReason = null + return + } Log.w( logTag, "wake timeout, interactive=${powerManager.isInteractive}, input=${InputService.isOpen}, reason:$reason" @@ -281,7 +284,11 @@ class MainService : Service() { wakeDidHome = true InputService.ctx?.goHome() } else if (!InputService.isOpen) { - Log.w(logTag, "input service not open, skip HOME, reason:$reason") + if (!wakeNotifiedInputDisabled) { + wakeNotifiedInputDisabled = true + Log.w(logTag, "input service not open, skip HOME, reason:$reason") + setTextNotification(null, "Remote session: enable Accessibility") + } } FFI.refreshScreen() @@ -299,6 +306,10 @@ class MainService : Service() { } private fun wakeAndRefreshIncomingScreenIfNeeded(reason: String) { + if (Looper.myLooper() != Looper.getMainLooper()) { + wakeRetryHandler.post { wakeAndRefreshIncomingScreenIfNeeded(reason) } + return + } if (powerManager.isInteractive) { return } @@ -316,6 +327,7 @@ class MainService : Service() { wakeDidHome = false wakeRetried = false wakeNotifiedLocked = false + wakeNotifiedInputDisabled = false wakeScreen(reason) wakeRetryHandler.postDelayed(wakePollRunnable, WAKE_POLL_INTERVAL_MS) } From 8e4571cb0e3b5c6cc2f9af0abe3abc71497f4858 Mon Sep 17 00:00:00 2001 From: Amirhossein Akhlaghpour Date: Tue, 28 Apr 2026 18:53:36 +0330 Subject: [PATCH 4/4] fix(android): restore foreground notification after wake Avoid leaving transient wake errors in the persistent foreground notification. --- .../src/main/kotlin/com/carriez/flutter_hbb/MainService.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index 1f8e71bf9..9a2466a23 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -292,6 +292,9 @@ class MainService : Service() { } FFI.refreshScreen() + if (wakeNotifiedLocked || wakeNotifiedInputDisabled) { + setTextNotification(null, null) + } wakePendingReason = null } } @@ -313,6 +316,10 @@ class MainService : Service() { if (powerManager.isInteractive) { return } + + // Reset any previous transient status text for a new attempt. + setTextNotification(null, null) + val now = SystemClock.elapsedRealtime() if (now - lastWakeAttemptAt < WAKE_THROTTLE_MS) { Log.i(logTag, "skip wake due to throttle, reason:$reason")