@@ -27,6 +27,8 @@ import com.enaboapps.switchify.service.window.overlay.OverlayTargets
2727import com.enaboapps.switchify.service.window.overlay.OverlayHandle
2828import com.enaboapps.switchify.service.window.overlay.SurfaceControlOverlayBackend
2929import 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 {
0 commit comments