Skip to content

Commit cc4a4e1

Browse files
authored
Use stable overlay identities and root-token SurfaceControl ownership
Merge stable overlay identity and SurfaceControl ownership changes for v2.35.6.
2 parents 4318a2b + baf80b1 commit cc4a4e1

11 files changed

Lines changed: 213 additions & 65 deletions

File tree

app/src/main/java/com/enaboapps/switchify/backend/preferences/PreferenceManager.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class PreferenceManager(context: Context) {
6060
const val PREFERENCE_KEY_TELEMETRY_ENABLED = "telemetry_enabled"
6161
const val PREFERENCE_KEY_DEVICE_ID = "device_id"
6262
const val PREFERENCE_KEY_LAST_PROCESS_EXIT_TIMESTAMP = "last_process_exit_timestamp"
63+
const val PREFERENCE_KEY_OVERLAY_SERVICE_EPOCH = "overlay_service_epoch"
6364
const val PREFERENCE_KEY_ONBOARDING_CURRENT_STEP = "onboarding_current_step"
6465
const val PREFERENCE_KEY_ONBOARDING_USER_TYPE = "onboarding_user_type"
6566
const val PREFERENCE_KEY_ONBOARDING_IS_NEW_USER = "onboarding_is_new_user"
@@ -107,6 +108,7 @@ class PreferenceManager(context: Context) {
107108
PREFERENCE_KEY_TELEMETRY_ENABLED,
108109
PREFERENCE_KEY_DEVICE_ID,
109110
PREFERENCE_KEY_LAST_PROCESS_EXIT_TIMESTAMP,
111+
PREFERENCE_KEY_OVERLAY_SERVICE_EPOCH,
110112
PREFERENCE_KEY_GEMMA_TERMS_ACCEPTED
111113
)
112114
}
@@ -268,6 +270,12 @@ class PreferenceManager(context: Context) {
268270
return generated
269271
}
270272

273+
fun nextOverlayServiceEpoch(): Long {
274+
val next = getLongValue(PREFERENCE_KEY_OVERLAY_SERVICE_EPOCH, 0L) + 1L
275+
setLongValue(PREFERENCE_KEY_OVERLAY_SERVICE_EPOCH, next)
276+
return next
277+
}
278+
271279
/**
272280
* Clears all whitelisted preferences (synced preferences) while preserving blacklisted keys.
273281
* Useful for logout/account deletion when user wants to clear their local data.

app/src/main/java/com/enaboapps/switchify/service/menu/MenuViewHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class MenuViewHandler {
1212
val instance: MenuViewHandler by lazy { MenuViewHandler() }
1313

1414
private const val TAG = "MenuViewHandler"
15-
private const val VIEW_ID = 1512
15+
internal const val VIEW_ID = 1512
1616
}
1717

1818
/** The base layout for the menu. */
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.enaboapps.switchify.service.window
2+
3+
internal data class OverlayDebugIdentity(
4+
val stableId: String,
5+
val serviceEpoch: Long,
6+
val generation: Int,
7+
val sequence: Long
8+
) {
9+
val diagnosticId: String
10+
get() = "$stableId:epoch=$serviceEpoch:generation=$generation:sequence=$sequence"
11+
}

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

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import androidx.savedstate.SavedStateRegistry
1717
import androidx.savedstate.SavedStateRegistryOwner
1818
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
1919
import com.enaboapps.switchify.service.core.SwitchifyLifecycleOwner
20+
import com.enaboapps.switchify.backend.preferences.PreferenceManager
2021
import com.enaboapps.switchify.service.llm.MediaPipeBackend
22+
import com.enaboapps.switchify.service.menu.MenuViewHandler
2123
import com.enaboapps.switchify.service.utils.ScreenWatcher
2224
import com.enaboapps.switchify.service.window.overlay.OverlayPlacement
2325
import com.enaboapps.switchify.service.window.overlay.OverlayDisplayMetrics
@@ -46,7 +48,9 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
4648
private var screenWatcher: ScreenWatcher? = null
4749
private var isVisible = false
4850
private var windowGeneration = 0
49-
private var rootOverlayId: Long? = null
51+
private var rootOverlayId: String? = null
52+
private var serviceEpoch = 0L
53+
private var overlaySequence = 0L
5054
private var acceptingOverlayOperations = false
5155
private val windowStateCleaner = WindowStateCleaner(
5256
onCleanupStarted = { state, rootAttached ->
@@ -92,7 +96,7 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
9296
val baseLayout: RelativeLayout?,
9397
val surfaceHandles: MutableMap<ViewGroup, OverlayHandle>,
9498
val wasVisible: Boolean,
95-
val rootOverlayId: Long?
99+
val rootOverlayId: String?
96100
)
97101

98102
private val defaultDisplayTarget = OverlayTargets.defaultDisplay().copy(forceSurface = true)
@@ -110,6 +114,8 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
110114
fun setup(context: Context) {
111115
try {
112116
acceptingOverlayOperations = false
117+
serviceEpoch = PreferenceManager(context.applicationContext).nextOverlayServiceEpoch()
118+
overlaySequence = 0L
113119
windowGeneration += 1
114120
if (!cleanupOnMainBlocking()) {
115121
Log.w(TAG, "Timed out while cleaning up previous window state before setup")
@@ -121,7 +127,9 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
121127
SurfaceControlOverlayBackend(
122128
service = service,
123129
hostTokenProvider = { baseLayout?.windowToken },
124-
generationProvider = { windowGeneration }
130+
identityProvider = { target, view ->
131+
nextOverlayIdentity(stableOverlayIdFor(target, view))
132+
}
125133
)
126134
}
127135
createBaseLayout()
@@ -222,15 +230,16 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
222230
)
223231
params.layoutInDisplayCutoutMode =
224232
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
225-
val title = "Switchify:root:generation=$windowGeneration"
233+
val identity = nextOverlayIdentity("root:default-display")
234+
val title = "Switchify:${identity.diagnosticId}"
226235
params.setTitle(title)
227236

228237
baseLayout?.let { layout ->
229238
// Add view with minimal overhead - lifecycle owners will be set up after
230239
windowManager?.addView(layout, params)
231240
isVisible = true
232241
rootOverlayId = SwitchifyOverlayDebugRegistry.recordRootShown(
233-
generation = windowGeneration,
242+
identity = identity,
234243
title = title,
235244
viewId = layout.id,
236245
viewClass = layout.javaClass.name,
@@ -385,11 +394,16 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
385394
postIfCurrentGeneration {
386395
try {
387396
if (shouldUseSurfaceBackend(target)) {
388-
val handle = surfaceControlBackend?.attach(target, view, placement)
389-
if (handle != null) {
390-
handle.setVisible(isVisible)
391-
surfaceOverlayHandles[view] = handle
392-
return@postIfCurrentGeneration
397+
if (baseLayout?.windowToken == null) {
398+
Log.w(TAG, "Surface overlay unavailable for $target because root token is missing")
399+
if (target is OverlayTarget.Window) return@postIfCurrentGeneration
400+
} else {
401+
val handle = surfaceControlBackend?.attach(target, view, placement)
402+
if (handle != null) {
403+
handle.setVisible(isVisible)
404+
surfaceOverlayHandles[view] = handle
405+
return@postIfCurrentGeneration
406+
}
393407
}
394408
if (target is OverlayTarget.Window) {
395409
Log.w(TAG, "Window overlay unavailable for $target; skipping fallback")
@@ -594,6 +608,27 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
594608
}
595609
}
596610

611+
private fun nextOverlayIdentity(stableId: String): OverlayDebugIdentity {
612+
overlaySequence += 1L
613+
return OverlayDebugIdentity(
614+
stableId = stableId,
615+
serviceEpoch = serviceEpoch,
616+
generation = windowGeneration,
617+
sequence = overlaySequence
618+
)
619+
}
620+
621+
private fun stableOverlayIdFor(target: OverlayTarget, view: ViewGroup): String {
622+
val role = when (view.id) {
623+
MenuViewHandler.VIEW_ID -> "menu"
624+
else -> view.javaClass.simpleName.ifEmpty { "view" }
625+
}
626+
return when (target) {
627+
is OverlayTarget.Display -> "surface:display:${target.displayId}:$role"
628+
is OverlayTarget.Window -> "surface:window:${target.displayId}:${target.accessibilityWindowId}:${target.windowType}:$role"
629+
}
630+
}
631+
597632
private fun cleanupOnMainBlocking(): Boolean {
598633
if (Looper.myLooper() == Looper.getMainLooper()) {
599634
cleanupCurrentStateNow()
@@ -660,7 +695,7 @@ class SwitchifyAccessibilityWindow private constructor() : LifecycleOwner, Saved
660695
private class AndroidWindowCleanupRoot(
661696
private val windowManager: WindowManager?,
662697
private val layout: RelativeLayout,
663-
override val debugOverlayId: Long?
698+
override val debugOverlayId: String?
664699
) : WindowCleanupRoot {
665700
override val isAttachedToWindow: Boolean
666701
get() = layout.parent != null

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

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
package com.enaboapps.switchify.service.window
22

3-
import java.util.concurrent.atomic.AtomicLong
4-
53
internal object SwitchifyOverlayDebugRegistry {
6-
private val nextId = AtomicLong(1L)
7-
private val records = linkedMapOf<Long, OverlayDebugRecord>()
4+
private val records = linkedMapOf<String, OverlayDebugRecord>()
85

96
fun recordRootShown(
10-
generation: Int,
7+
identity: OverlayDebugIdentity,
118
title: String,
129
viewId: Int,
1310
viewClass: String,
1411
handleIdentityHash: Int
15-
): Long {
16-
val id = nextId.getAndIncrement()
12+
): String {
13+
val id = identity.diagnosticId
1714
synchronized(records) {
1815
records[id] = OverlayDebugRecord(
1916
id = id,
17+
stableId = identity.stableId,
2018
backend = BACKEND_WINDOW_MANAGER_ROOT,
21-
generation = generation,
19+
serviceEpoch = identity.serviceEpoch,
20+
generation = identity.generation,
21+
sequence = identity.sequence,
2222
target = title,
2323
viewId = viewId,
2424
viewClass = viewClass,
@@ -29,25 +29,28 @@ internal object SwitchifyOverlayDebugRegistry {
2929
return id
3030
}
3131

32-
fun recordRootRemoved(id: Long?) {
32+
fun recordRootRemoved(id: String?) {
3333
recordReleased(id)
3434
}
3535

3636
fun recordSurfaceAttached(
37-
id: Long,
37+
identity: OverlayDebugIdentity,
3838
backend: String,
39-
generation: Int,
4039
target: String,
4140
viewId: Int,
4241
viewClass: String,
4342
handleIdentityHash: Int,
4443
surface: String
4544
) {
45+
val id = identity.diagnosticId
4646
synchronized(records) {
4747
records[id] = OverlayDebugRecord(
4848
id = id,
49+
stableId = identity.stableId,
4950
backend = backend,
50-
generation = generation,
51+
serviceEpoch = identity.serviceEpoch,
52+
generation = identity.generation,
53+
sequence = identity.sequence,
5154
target = target,
5255
viewId = viewId,
5356
viewClass = viewClass,
@@ -58,11 +61,11 @@ internal object SwitchifyOverlayDebugRegistry {
5861
}
5962
}
6063

61-
fun recordSurfaceReleased(id: Long) {
64+
fun recordSurfaceReleased(id: String) {
6265
recordReleased(id)
6366
}
6467

65-
fun recordSurfaceReleaseFailed(id: Long, throwable: Throwable) {
68+
fun recordSurfaceReleaseFailed(id: String, throwable: Throwable) {
6669
synchronized(records) {
6770
records[id] = records[id]?.copy(
6871
releaseFailure = "${throwable.javaClass.simpleName}: ${throwable.message}"
@@ -77,8 +80,11 @@ internal object SwitchifyOverlayDebugRegistry {
7780
appendLine("Switchify overlay registry:")
7881
snapshot.forEach { record ->
7982
append("id=").append(record.id)
83+
.append(" stableId=").append(record.stableId)
8084
.append(" backend=").append(record.backend)
85+
.append(" serviceEpoch=").append(record.serviceEpoch)
8186
.append(" generation=").append(record.generation)
87+
.append(" sequence=").append(record.sequence)
8288
.append(" target=").append(record.target)
8389
.append(" viewId=").append(record.viewId)
8490
.append(" viewClass=").append(record.viewClass)
@@ -103,14 +109,9 @@ internal object SwitchifyOverlayDebugRegistry {
103109
synchronized(records) {
104110
records.clear()
105111
}
106-
nextId.set(1L)
107-
}
108-
109-
fun nextOverlayId(): Long {
110-
return nextId.getAndIncrement()
111112
}
112113

113-
private fun recordReleased(id: Long?) {
114+
private fun recordReleased(id: String?) {
114115
if (id == null) return
115116
synchronized(records) {
116117
records[id] = records[id]?.copy(
@@ -125,9 +126,12 @@ internal object SwitchifyOverlayDebugRegistry {
125126
internal const val BACKEND_SURFACE_CONTROL_WINDOW = "surface_control_window"
126127

127128
private data class OverlayDebugRecord(
128-
val id: Long,
129+
val id: String,
130+
val stableId: String,
129131
val backend: String,
132+
val serviceEpoch: Long,
130133
val generation: Int,
134+
val sequence: Long,
131135
val target: String,
132136
val viewId: Int,
133137
val viewClass: String,

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ internal interface WindowCleanupHandle {
1515

1616
internal interface WindowCleanupRoot {
1717
val isAttachedToWindow: Boolean
18-
val debugOverlayId: Long?
18+
val debugOverlayId: String?
1919

2020
fun removeDescendantViews()
2121
fun removeImmediately()
@@ -31,9 +31,9 @@ internal class WindowStateCleaner(
3131
private val onCleanupStarted: (WindowCleanupState, Boolean) -> Unit = { _, _ -> },
3232
private val onHandleReleased: (Any) -> Unit = {},
3333
private val onHandleReleaseFailed: (Any, Throwable) -> Unit = { _, _ -> },
34-
private val onRootRemoved: (Long?) -> Unit = {},
35-
private val onRootAlreadyRemoved: (Long?, Throwable) -> Unit = { _, _ -> },
36-
private val onRootRemoveFailed: (Long?, Throwable) -> Unit = { _, _ -> }
34+
private val onRootRemoved: (String?) -> Unit = {},
35+
private val onRootAlreadyRemoved: (String?, Throwable) -> Unit = { _, _ -> },
36+
private val onRootRemoveFailed: (String?, Throwable) -> Unit = { _, _ -> }
3737
) {
3838
fun cleanup(state: WindowCleanupState) {
3939
val rootAttachedAtCapture = state.root?.isAttachedToWindow == true

0 commit comments

Comments
 (0)