Skip to content

Commit 4a2883f

Browse files
Clean up service overlays on interruption (#2567)
- Release default and SurfaceControl overlays during service shutdown - Guard queued overlay operations by service window generation - Tear down overlay windows during service unbind as well as destroy 🤖 Auto-generated Co-authored-by: Owen McGirr <o.a.mcgirr@gmail.com>
1 parent 8a04f26 commit 4a2883f

3 files changed

Lines changed: 101 additions & 44 deletions

File tree

app/src/main/java/com/enaboapps/switchify/service/core/SwitchifyAccessibilityService.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ class SwitchifyAccessibilityService : AccessibilityService(), LifecycleOwner,
321321
eventPipeline.stop()
322322
}
323323
ServiceCore.cleanup()
324+
SwitchifyAccessibilityWindow.instance.onServiceDestroy()
324325
PcMouseRepeatManager.instance.clearServiceState()
325326
GestureRepeatManager.instance.clearServiceState()
326327
GestureLockManager.instance.clearServiceState()

app/src/main/java/com/enaboapps/switchify/service/window/SwitchifyAccessibilityWindow.kt

Lines changed: 84 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import com.enaboapps.switchify.service.window.overlay.OverlayTargets
2727
import com.enaboapps.switchify.service.window.overlay.OverlayHandle
2828
import com.enaboapps.switchify.service.window.overlay.SurfaceControlOverlayBackend
2929
import com.enaboapps.switchify.service.window.overlay.SwitchifyOverlayHost
30+
import java.util.concurrent.CountDownLatch
31+
import java.util.concurrent.TimeUnit
3032

3133
/**
3234
* This class manages the window for the Switchify accessibility service.
@@ -43,9 +45,12 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
4345
private val mainHandler = Handler(Looper.getMainLooper())
4446
private var screenWatcher: ScreenWatcher? = null
4547
private var isVisible = false
48+
private var windowGeneration = 0
49+
private var acceptingOverlayOperations = false
4650

4751
companion object {
4852
private const val TAG = "SwitchifyAccessibilityWindow"
53+
private const val SHUTDOWN_CLEANUP_TIMEOUT_MS = 500L
4954
val instance: SwitchifyAccessibilityWindow by lazy {
5055
SwitchifyAccessibilityWindow()
5156
}
@@ -65,15 +70,17 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
6570
*/
6671
fun setup(context: Context) {
6772
try {
68-
// Clean up previous state if it exists
69-
cleanup()
73+
acceptingOverlayOperations = false
74+
windowGeneration += 1
75+
cleanupOnMainBlocking()
7076

7177
this.context = context
7278
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
7379
surfaceControlBackend = (context as? AccessibilityService)?.let { service ->
7480
SurfaceControlOverlayBackend(service) { baseLayout?.windowToken }
7581
}
7682
createBaseLayout()
83+
acceptingOverlayOperations = true
7784
registerScreenWatcher()
7885
ServiceMessageHUD.instance.setup(context.applicationContext)
7986
MenuHighlightHud.instance.setup(context.applicationContext)
@@ -154,12 +161,11 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
154161
* Shows the window.
155162
*/
156163
fun show() {
157-
mainHandler.post {
164+
postIfCurrentGeneration {
158165
try {
159-
// Avoid adding duplicate views
160166
if (isVisible) {
161167
Log.d(TAG, "Window already visible, skipping show()")
162-
return@post
168+
return@postIfCurrentGeneration
163169
}
164170

165171
val params = WindowManager.LayoutParams(
@@ -190,7 +196,7 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
190196
* Hides the window without cleaning up views.
191197
*/
192198
fun hide() {
193-
mainHandler.post {
199+
postIfCurrentGeneration {
194200
try {
195201
if (isVisible && baseLayout != null) {
196202
windowManager?.removeView(baseLayout)
@@ -208,41 +214,26 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
208214
* Cleans up the window and its resources.
209215
*/
210216
fun cleanup() {
211-
mainHandler.post {
212-
try {
213-
for (i in 0 until (baseLayout?.childCount ?: 0)) {
214-
val child = baseLayout?.getChildAt(i)
215-
if (child is ViewGroup) {
216-
child.removeAllViews()
217-
}
218-
}
219-
220-
if (isVisible && baseLayout != null) {
221-
windowManager?.removeView(baseLayout)
222-
isVisible = false
223-
}
224-
225-
// Clear all children from baseLayout
226-
baseLayout?.removeAllViews()
227-
releaseSurfaceOverlays()
228-
} catch (e: Exception) {
229-
Log.e(TAG, "Error in cleanup: ${e.message}", e)
230-
}
217+
if (Looper.myLooper() == Looper.getMainLooper()) {
218+
if (acceptingOverlayOperations) cleanupNow()
219+
return
231220
}
221+
postIfCurrentGeneration { cleanupNow() }
232222
}
233223

234224
/**
235225
* Cleans up the window and its resources when the service is destroyed.
236226
*/
237227
fun onServiceDestroy() {
228+
acceptingOverlayOperations = false
238229
ServiceMessageHUD.instance.dispose()
239230
MenuHighlightHud.instance.dispose()
240231
ServiceStartupSplash.instance.dispose()
241232
MediaPipeBackend.close()
242-
cleanup()
243-
isVisible = false // Ensure the flag is set to false for the next time the window is created
244-
val ctx = getContext() ?: return
245-
screenWatcher?.unregister(ctx)
233+
cleanupForServiceShutdown()
234+
getContext()?.let { ctx ->
235+
screenWatcher?.unregister(ctx)
236+
}
246237
screenWatcher = null
247238
context = null
248239
windowManager = null
@@ -324,22 +315,22 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
324315
view: ViewGroup,
325316
placement: OverlayPlacement
326317
) {
327-
mainHandler.post {
318+
postIfCurrentGeneration {
328319
try {
329320
if (shouldUseSurfaceBackend(target)) {
330321
val handle = surfaceControlBackend?.attach(target, view, placement)
331322
if (handle != null) {
332323
handle.setVisible(isVisible)
333324
surfaceOverlayHandles[view] = handle
334-
return@post
325+
return@postIfCurrentGeneration
335326
}
336327
if (target is OverlayTarget.Window) {
337328
Log.w(TAG, "Window overlay unavailable for $target; skipping fallback")
338-
return@post
329+
return@postIfCurrentGeneration
339330
}
340331
if (!canUseDefaultRootFallback(target)) {
341332
Log.w(TAG, "Surface overlay unavailable for $target; no default-root fallback")
342-
return@post
333+
return@postIfCurrentGeneration
343334
}
344335
Log.w(TAG, "Surface overlay unavailable for $target; falling back to default overlay")
345336
}
@@ -469,11 +460,11 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
469460
}
470461

471462
fun removeView(target: OverlayTarget, view: ViewGroup) {
472-
mainHandler.post {
463+
postIfCurrentGeneration {
473464
try {
474465
surfaceOverlayHandles.remove(view)?.let { handle ->
475466
handle.release()
476-
return@post
467+
return@postIfCurrentGeneration
477468
}
478469
if (view.parent == baseLayout) {
479470
baseLayout?.removeView(view)
@@ -495,11 +486,11 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
495486
}
496487

497488
override fun removeView(target: OverlayTarget.Display, id: Int) {
498-
mainHandler.post {
489+
postIfCurrentGeneration {
499490
try {
500491
surfaceOverlayHandles.entries.firstOrNull { it.key.id == id }?.let { entry ->
501492
surfaceOverlayHandles.remove(entry.key)?.release()
502-
return@post
493+
return@postIfCurrentGeneration
503494
}
504495
baseLayout?.findViewById<ViewGroup>(id)?.let { view ->
505496
baseLayout?.removeView(view)
@@ -528,6 +519,61 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
528519
surfaceControlBackend?.canAttach(target) == true
529520
}
530521

522+
private fun postIfCurrentGeneration(block: () -> Unit) {
523+
val generation = windowGeneration
524+
mainHandler.post {
525+
if (generation != windowGeneration || !acceptingOverlayOperations) return@post
526+
block()
527+
}
528+
}
529+
530+
private fun cleanupForServiceShutdown() {
531+
acceptingOverlayOperations = false
532+
cleanupOnMainBlocking()
533+
}
534+
535+
private fun cleanupOnMainBlocking() {
536+
if (Looper.myLooper() == Looper.getMainLooper()) {
537+
cleanupNow()
538+
return
539+
}
540+
val latch = CountDownLatch(1)
541+
mainHandler.postAtFrontOfQueue {
542+
try {
543+
cleanupNow()
544+
} finally {
545+
latch.countDown()
546+
}
547+
}
548+
if (!latch.await(SHUTDOWN_CLEANUP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
549+
Log.w(TAG, "Timed out waiting for window cleanup")
550+
}
551+
}
552+
553+
private fun cleanupNow() {
554+
try {
555+
releaseSurfaceOverlays()
556+
for (i in 0 until (baseLayout?.childCount ?: 0)) {
557+
val child = baseLayout?.getChildAt(i)
558+
if (child is ViewGroup) {
559+
child.removeAllViews()
560+
}
561+
}
562+
baseLayout?.removeAllViews()
563+
baseLayout?.let { layout ->
564+
if (isVisible || layout.parent != null) {
565+
windowManager?.removeViewImmediate(layout)
566+
}
567+
}
568+
isVisible = false
569+
} catch (e: IllegalArgumentException) {
570+
isVisible = false
571+
Log.w(TAG, "Window was already removed during cleanup", e)
572+
} catch (e: Exception) {
573+
Log.e(TAG, "Error in cleanup: ${e.message}", e)
574+
}
575+
}
576+
531577
private fun releaseSurfaceOverlays() {
532578
surfaceOverlayHandles.values.forEach { handle ->
533579
try {

app/src/main/java/com/enaboapps/switchify/service/window/overlay/SurfaceControlOverlayBackend.kt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,20 +196,30 @@ class SurfaceControlOverlayBackend(
196196
private val surfaceControl: SurfaceControl,
197197
private val viewHost: SurfaceControlViewHost
198198
) : OverlayHandle {
199+
private var released = false
200+
199201
override fun setVisible(visible: Boolean) {
202+
if (released) return
200203
SurfaceControl.Transaction()
201204
.setVisibility(surfaceControl, visible)
202205
.apply()
203206
}
204207

205208
override fun release() {
206-
if (view.parent === container) {
207-
container.removeView(view)
209+
if (released) return
210+
released = true
211+
try {
212+
if (view.parent === container) {
213+
container.removeView(view)
214+
}
215+
SurfaceControl.Transaction()
216+
.reparent(surfaceControl, null)
217+
.apply()
218+
} catch (e: Exception) {
219+
Log.e(TAG, "Failed to release SurfaceControl overlay", e)
220+
} finally {
221+
viewHost.release()
208222
}
209-
SurfaceControl.Transaction()
210-
.reparent(surfaceControl, null)
211-
.apply()
212-
viewHost.release()
213223
}
214224
}
215225

0 commit comments

Comments
 (0)