From 0f41455d788d436b571094c91998acb22bf30f07 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 12 May 2026 21:28:05 +0530 Subject: [PATCH] fix: use correct window context for debug overlay Signed-off-by: Akash Yadav --- .../activities/editor/BaseEditorActivity.kt | 181 +++++---- .../services/debug/DebugOverlayManager.kt | 343 ++++++++++-------- .../services/debug/DebuggerService.kt | 49 ++- 3 files changed, 329 insertions(+), 244 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt index 8964fb74b3..dda5a1c16e 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt @@ -52,7 +52,9 @@ import androidx.annotation.RequiresApi import androidx.annotation.UiThread import androidx.appcompat.app.ActionBarDrawerToggle import androidx.collection.MutableIntIntMap +import androidx.core.content.ContextCompat import androidx.core.graphics.Insets +import androidx.core.hardware.display.DisplayManagerCompat import androidx.core.view.GravityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat @@ -83,6 +85,7 @@ import com.itsaky.androidide.adapters.DiagnosticsAdapter import com.itsaky.androidide.adapters.SearchListAdapter import com.itsaky.androidide.api.BuildOutputProvider import com.itsaky.androidide.app.EdgeToEdgeIDEActivity +import com.itsaky.androidide.app.IDEApplication import com.itsaky.androidide.databinding.ActivityEditorBinding import com.itsaky.androidide.databinding.ContentEditorBinding import com.itsaky.androidide.databinding.LayoutDiagnosticInfoBinding @@ -104,7 +107,6 @@ import com.itsaky.androidide.models.DiagnosticGroup import com.itsaky.androidide.models.OpenedFile import com.itsaky.androidide.models.Range import com.itsaky.androidide.models.SearchResult -import com.itsaky.androidide.app.IDEApplication import com.itsaky.androidide.plugins.manager.ui.PluginEditorTabManager import com.itsaky.androidide.preferences.internal.BuildPreferences import com.itsaky.androidide.projects.IProjectManager @@ -123,10 +125,10 @@ import com.itsaky.androidide.utils.IntentUtils import com.itsaky.androidide.utils.MemoryUsageWatcher import com.itsaky.androidide.utils.StringsInjectionException import com.itsaky.androidide.utils.StringsXmlInjector -import com.itsaky.androidide.utils.applyResponsiveAppBarInsets +import com.itsaky.androidide.utils.applyBottomSheetAnchorForOrientation import com.itsaky.androidide.utils.applyImmersiveModeInsets +import com.itsaky.androidide.utils.applyResponsiveAppBarInsets import com.itsaky.androidide.utils.applyRootSystemInsetsAsPadding -import com.itsaky.androidide.utils.applyBottomSheetAnchorForOrientation import com.itsaky.androidide.utils.flashError import com.itsaky.androidide.utils.flashMessage import com.itsaky.androidide.utils.getOrStoreInitialPadding @@ -208,7 +210,8 @@ abstract class BaseEditorActivity : @Suppress("ktlint:standard:backing-property-naming") internal var _binding: ActivityEditorBinding? = null val binding: ActivityEditorBinding - get() = _binding ?: throw IllegalStateException("Activity destroyed; binding not accessible") + get() = _binding + ?: throw IllegalStateException("Activity destroyed; binding not accessible") val content: ContentEditorBinding get() = binding.content @@ -230,12 +233,15 @@ abstract class BaseEditorActivity : binding.editorDrawerLayout.isDrawerOpen(GravityCompat.START) -> { binding.editorDrawerLayout.closeDrawer(GravityCompat.START) } + bottomSheetViewModel.sheetBehaviorState != BottomSheetBehavior.STATE_COLLAPSED -> { bottomSheetViewModel.setSheetState(sheetState = BottomSheetBehavior.STATE_COLLAPSED) } + binding.swipeReveal.isOpen -> { binding.swipeReveal.close() } + else -> { doConfirmProjectClose() } @@ -249,7 +255,12 @@ abstract class BaseEditorActivity : memoryUsage.forEachValue { proc -> _binding?.memUsageView?.chart?.apply { val dataset = - (data.getDataSetByIndex(pidToDatasetIdxMap.getOrDefault(proc.pid, -1)) as LineDataSet?) + (data.getDataSetByIndex( + pidToDatasetIdxMap.getOrDefault( + proc.pid, + -1 + ) + ) as LineDataSet?) ?: run { log.error( "No dataset found for process: {}: {}", @@ -360,6 +371,12 @@ abstract class BaseEditorActivity : isDebuggerStarting = true val intent = Intent(this, DebuggerService::class.java) + .apply { + val currentOrDefaultDisplay = + ContextCompat.getDisplayOrDefault(this@BaseEditorActivity) + putExtra(DebuggerService.EXTRA_DISPLAY_ID, currentOrDefaultDisplay.displayId) + } + if (bindService(intent, debuggerServiceConnection, BIND_AUTO_CREATE)) { postStopDebuggerServiceIfNotConnected() doSetStatus(getString(string.debugger_starting)) @@ -387,7 +404,6 @@ abstract class BaseEditorActivity : private var editorAppBarInsetTop: Int = 0 companion object { - private const val TAG = "ResizePanelDebugger" const val DEBUGGER_SERVICE_STOP_DELAY_MS: Long = 60 * 1000 @@ -527,8 +543,11 @@ abstract class BaseEditorActivity : } } } + else -> { - updateLayoutParams { height = ViewGroup.LayoutParams.MATCH_PARENT } + updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + } post { contentCardRealHeight = measuredHeight } } } @@ -602,7 +621,8 @@ abstract class BaseEditorActivity : } override fun onCreate(savedInstanceState: Bundle?) { - savedInstanceState?.getString(KEY_PROJECT_PATH)?.let(ProjectManagerImpl.getInstance()::projectPath::set) + savedInstanceState?.getString(KEY_PROJECT_PATH) + ?.let(ProjectManagerImpl.getInstance()::projectPath::set) super.onCreate(savedInstanceState) editorViewModel.isBuildInProgress = false @@ -766,7 +786,8 @@ abstract class BaseEditorActivity : val insetsTop = systemBarInsets?.top ?: 0 val topInset = (insetsTop * (1f - progress)).roundToInt() - val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val isLandscape = + resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE if (isLandscape) { content.editorAppbarContent.updatePadding(top = topInset) @@ -875,6 +896,13 @@ abstract class BaseEditorActivity : super.onResume() invalidateOptionsMenu() + val displayId = ContextCompat.getDisplayOrDefault(this).displayId + runCatching { + debuggerService?.maybeMoveOverlayToDisplay(displayId) + }.onFailure { err -> + log.warn("Unable to move debugger overlay to display {}", displayId, err) + } + memoryUsageWatcher.listener = memoryUsageListener memoryUsageWatcher.startWatching() @@ -1091,66 +1119,70 @@ abstract class BaseEditorActivity : private fun handleUiDesignerResult(result: ActivityResult) { if (result.resultCode != RESULT_OK || result.data == null) { - log.warn("UI Designer returned invalid result: resultCode={}, data={}", result.resultCode, result.data) - return - } + log.warn( + "UI Designer returned invalid result: resultCode={}, data={}", + result.resultCode, + result.data + ) + return + } - val data = result.data!! - val generatedXml = data.getStringExtra(UIDesignerActivity.RESULT_GENERATED_XML) + val data = result.data!! + val generatedXml = data.getStringExtra(UIDesignerActivity.RESULT_GENERATED_XML) - if (TextUtils.isEmpty(generatedXml)) { - log.warn("UI Designer returned blank generated XML code") - return - } + if (TextUtils.isEmpty(generatedXml)) { + log.warn("UI Designer returned blank generated XML code") + return + } - editorActivityScope.launch { - val injectionSuccess = handleStringsInjection(data) + editorActivityScope.launch { + val injectionSuccess = handleStringsInjection(data) - if (injectionSuccess) { - withContext(Dispatchers.Main) { applyGeneratedXmlToEditor(generatedXml!!) } - } else { - log.warn("Aborting layout update due to string injection failure.") - } - } + if (injectionSuccess) { + withContext(Dispatchers.Main) { applyGeneratedXmlToEditor(generatedXml!!) } + } else { + log.warn("Aborting layout update due to string injection failure.") + } + } } private suspend fun handleStringsInjection(data: Intent): Boolean { - val stringsXml = data.getStringExtra(UIDesignerActivity.EXTRA_GENERATED_STRINGS) - val layoutFilePath = data.getStringExtra(UIDesignerActivity.EXTRA_LAYOUT_FILE_PATH) - - if (stringsXml.isNullOrBlank()) return true - - if (layoutFilePath.isNullOrBlank()) { - log.warn("Skipping string injection: generated strings present but layout file path is missing.") - return false - } - - val result = StringsXmlInjector.inject(layoutFilePath, stringsXml) - - result.onFailure { error -> - log.error("String injection failed", error) - withContext(Dispatchers.Main) { - val message = when (error) { - is StringsInjectionException -> getString(error.messageRes) - else -> getString(string.msg_strings_injection_failed) - } - flashError(message) - } - } - - return result.isSuccess - } - - private fun applyGeneratedXmlToEditor(generatedXml: String) { - val view = provideCurrentEditor() - val text = view?.editor?.text ?: run { - log.warn("No file opened to append UI designer result") - return - } - - val endLine = text.lineCount - 1 - text.replace(0, 0, endLine, text.getColumnCount(endLine), generatedXml) - } + val stringsXml = data.getStringExtra(UIDesignerActivity.EXTRA_GENERATED_STRINGS) + val layoutFilePath = data.getStringExtra(UIDesignerActivity.EXTRA_LAYOUT_FILE_PATH) + + if (stringsXml.isNullOrBlank()) return true + + if (layoutFilePath.isNullOrBlank()) { + log.warn("Skipping string injection: generated strings present but layout file path is missing.") + return false + } + + val result = StringsXmlInjector.inject(layoutFilePath, stringsXml) + + result.onFailure { error -> + log.error("String injection failed", error) + withContext(Dispatchers.Main) { + val message = when (error) { + is StringsInjectionException -> getString(error.messageRes) + else -> getString(string.msg_strings_injection_failed) + } + flashError(message) + } + } + + return result.isSuccess + } + + private fun applyGeneratedXmlToEditor(generatedXml: String) { + val view = provideCurrentEditor() + val text = view?.editor?.text ?: run { + log.warn("No file opened to append UI designer result") + return + } + + val endLine = text.lineCount - 1 + text.replace(0, 0, endLine, text.getColumnCount(endLine), generatedXml) + } private fun setupDrawers() { // Note: Drawer toggle is now set up in setupToolbar() on the title toolbar @@ -1177,6 +1209,7 @@ abstract class BaseEditorActivity : content.progressIndicator.visibility = if (visible) View.VISIBLE else View.GONE invalidateOptionsMenu() } + private fun setupStateObservers() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.CREATED) { @@ -1438,9 +1471,9 @@ abstract class BaseEditorActivity : it.realContainer.pivotX = it.realContainer.width.toFloat() / 2f it.realContainer.pivotY = (it.realContainer.height.toFloat() / 2f) + ( - systemBarInsets?.run { bottom - top } - ?: 0 - ) + systemBarInsets?.run { bottom - top } + ?: 0 + ) it.viewContainer.viewTreeObserver.removeOnGlobalLayoutListener(this) } } @@ -1524,16 +1557,16 @@ abstract class BaseEditorActivity : val startedNearTopEdge = e1.y < topEdgeThreshold val startedNearBottomEdge = e1.y > bottomEdgeThreshold val isTopEdgeDismissFling = isVerticalSwipe && - hasVerticalVelocity && - startedNearTopEdge && - hasDownFlingDistance + hasVerticalVelocity && + startedNearTopEdge && + hasDownFlingDistance val isBottomEdgeDismissFling = isVerticalSwipe && - hasVerticalVelocity && - startedNearBottomEdge && - hasUpFlingDistance + hasVerticalVelocity && + startedNearBottomEdge && + hasUpFlingDistance val isDrawerOpenFling = hasRightFlingDistance && - hasHorizontalVelocity && - isHorizontalSwipe + hasHorizontalVelocity && + isHorizontalSwipe // Fullscreen mode can be dismissed with an inward fling from either vertical edge. if (isTopEdgeDismissFling && editorViewModel.isFullscreen) { @@ -1549,7 +1582,7 @@ abstract class BaseEditorActivity : // Preserve the editor interaction area; drawer gestures are only enabled on the empty state. val noFilesOpen = content.viewContainer.displayedChild == 1 if (!noFilesOpen) { - return false + return false } // Filter out diagonal flings so only an intentional right swipe opens the drawer. diff --git a/app/src/main/java/com/itsaky/androidide/services/debug/DebugOverlayManager.kt b/app/src/main/java/com/itsaky/androidide/services/debug/DebugOverlayManager.kt index 6f18a1c6c7..42652f598a 100644 --- a/app/src/main/java/com/itsaky/androidide/services/debug/DebugOverlayManager.kt +++ b/app/src/main/java/com/itsaky/androidide/services/debug/DebugOverlayManager.kt @@ -2,6 +2,7 @@ package com.itsaky.androidide.services.debug import android.content.Context import android.graphics.PixelFormat +import android.hardware.display.DisplayManager import android.view.ContextThemeWrapper import android.view.Gravity import android.view.LayoutInflater @@ -11,6 +12,7 @@ import android.view.WindowManager import android.widget.Toast import android.provider.Settings import androidx.core.content.ContextCompat +import androidx.core.hardware.display.DisplayManagerCompat import com.itsaky.androidide.R import com.itsaky.androidide.actions.ActionItem import com.itsaky.androidide.actions.ActionsRegistry @@ -18,6 +20,7 @@ import com.itsaky.androidide.databinding.DebuggerActionsWindowBinding import com.itsaky.androidide.idetooltips.TooltipManager import com.itsaky.androidide.idetooltips.TooltipTag import com.itsaky.androidide.utils.PermissionsHelper +import com.itsaky.androidide.utils.isAtLeastR import org.slf4j.LoggerFactory import kotlin.math.abs @@ -27,164 +30,186 @@ import kotlin.math.abs * @author Akash Yadav */ class DebugOverlayManager private constructor( - private val windowManager: WindowManager, - private val binding: DebuggerActionsWindowBinding, - private val touchSlop: Int = ViewConfiguration.get(binding.root.context).scaledTouchSlop, + private val windowManager: WindowManager, + private val binding: DebuggerActionsWindowBinding, + val attachedDisplayId: Int, + private val touchSlop: Int = ViewConfiguration.get(binding.root.context).scaledTouchSlop, ) { - private var initialWindowX = 0 - private var initialWindowY = 0 - private var initialTouchX = 0f - private var initialTouchY = 0f - private var isDragging = false - - init { - binding.dragHandle.root.isLongClickable = true - binding.dragHandle.root.icon = ContextCompat.getDrawable(binding.root.context, R.drawable.ic_drag_handle) - - binding.dragHandle.root.setOnTouchListener { v, event -> - when(event.action) { - MotionEvent.ACTION_DOWN -> { - initialWindowX = overlayLayoutParams.x - initialWindowY = overlayLayoutParams.y - initialTouchX = event.rawX - initialTouchY = event.rawY - isDragging = false - false - } - - MotionEvent.ACTION_MOVE -> { - val deltaX = (event.rawX - initialTouchX).toInt() - val deltaY = (event.rawY - initialTouchY).toInt() - if (isDragging || abs(deltaX) > touchSlop || abs(deltaY) > touchSlop) { - isDragging = true - v.isPressed = false - v.parent.requestDisallowInterceptTouchEvent(true) - overlayLayoutParams.x = initialWindowX + deltaX - overlayLayoutParams.y = initialWindowY + deltaY - windowManager.updateViewLayout(binding.root, overlayLayoutParams) - true - } else false - } - - MotionEvent.ACTION_UP -> { - if (isDragging) { - isDragging = false - true - } else { - v.performClick() - false - }} - - MotionEvent.ACTION_CANCEL -> { - isDragging = false - false - } - - else -> false - } - } - - binding.dragHandle.root.setOnLongClickListener { view -> - TooltipManager.showIdeCategoryTooltip( - context = view.context, - anchorView = view.rootView, - tag = TooltipTag.DEBUGGER_ACTION_MOVE - ) - true - } - } - - private var isShown = false - private val overlayLayoutParams by lazy { - WindowManager.LayoutParams( - /* w = */ WindowManager.LayoutParams.WRAP_CONTENT, - /* h = */ WindowManager.LayoutParams.WRAP_CONTENT, - /* _type = */ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, - /* _flags = */ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - /* _format = */ PixelFormat.TRANSLUCENT - ).apply { - gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL - } - } - - fun show() { - if (isShown) { - return - } - - val ctx = binding.root.context - - if (!Settings.canDrawOverlays(ctx)) { - logger.warn("Overlay permission denied. Skipping debugger overlay window.") - - val state = PermissionsHelper.getOverlayPermissionState(ctx) - val message = if (state == PermissionsHelper.OverlayPermissionState.UNSUPPORTED) { - ctx.getString(R.string.permission_overlay_unsupported_hint) - } else { - ctx.getString(R.string.permission_overlay_restricted_settings_hint) - } - Toast.makeText(ctx, message, Toast.LENGTH_LONG).show() - return - } - - try { - windowManager.addView(binding.root, overlayLayoutParams) - isShown = true - } catch (err: Throwable) { - logger.error("Failed to show debugger overlay window", err) - } - } - - fun hide() { - if (!isShown) { - return - } - - try { - windowManager.removeView(binding.root) - } catch (err: Throwable) { - logger.error("Failed to hide debugger overlay window", err) - } finally { - isShown = false - } - } - - fun refreshActions() { - // noinspection NotifyDataSetChanged - binding.actions.adapter?.notifyDataSetChanged() - } - - companion object { - - private val logger = LoggerFactory.getLogger(DebugOverlayManager::class.java) - - /** - * Create a new [DebugOverlayManager] from the given [Context]. - * - * @param ctx The [Context] to use for creating the [DebugOverlayManager]. - * @return A new [DebugOverlayManager]. - */ - fun create(ctx: Context): DebugOverlayManager { - // IMPORTANT! - // Wrap the context with a theme, so we could use MaterialButtons! - val context = ContextThemeWrapper(ctx, R.style.Theme_AndroidIDE) - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - val inflater = LayoutInflater.from(context) - - // noinspection InflateParams - val layout = DebuggerActionsWindowBinding.inflate(inflater) - - val actionsRegistry = ActionsRegistry.getInstance() - val debuggerActions = actionsRegistry.getActions(ActionItem.Location.DEBUGGER_ACTIONS) - - val actions = debuggerActions.values.toList() - val adapter = DebuggerActionsOverlayAdapter(actions) - layout.actions.adapter = adapter - - return DebugOverlayManager( - windowManager = windowManager, - binding = layout, - ) - } - } + private var initialWindowX = 0 + private var initialWindowY = 0 + private var initialTouchX = 0f + private var initialTouchY = 0f + private var isDragging = false + + init { + binding.dragHandle.root.isLongClickable = true + binding.dragHandle.root.icon = + ContextCompat.getDrawable(binding.root.context, R.drawable.ic_drag_handle) + + binding.dragHandle.root.setOnTouchListener { v, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + initialWindowX = overlayLayoutParams.x + initialWindowY = overlayLayoutParams.y + initialTouchX = event.rawX + initialTouchY = event.rawY + isDragging = false + false + } + + MotionEvent.ACTION_MOVE -> { + val deltaX = (event.rawX - initialTouchX).toInt() + val deltaY = (event.rawY - initialTouchY).toInt() + if (isDragging || abs(deltaX) > touchSlop || abs(deltaY) > touchSlop) { + isDragging = true + v.isPressed = false + v.parent.requestDisallowInterceptTouchEvent(true) + overlayLayoutParams.x = initialWindowX + deltaX + overlayLayoutParams.y = initialWindowY + deltaY + windowManager.updateViewLayout(binding.root, overlayLayoutParams) + true + } else false + } + + MotionEvent.ACTION_UP -> { + if (isDragging) { + isDragging = false + true + } else { + v.performClick() + false + } + } + + MotionEvent.ACTION_CANCEL -> { + isDragging = false + false + } + + else -> false + } + } + + binding.dragHandle.root.setOnLongClickListener { view -> + TooltipManager.showIdeCategoryTooltip( + context = view.context, + anchorView = view.rootView, + tag = TooltipTag.DEBUGGER_ACTION_MOVE + ) + true + } + } + + private var isShown = false + private val overlayLayoutParams by lazy { + WindowManager.LayoutParams( + /* w = */ WindowManager.LayoutParams.WRAP_CONTENT, + /* h = */ WindowManager.LayoutParams.WRAP_CONTENT, + /* _type = */ WM_OVERLAY_TYPE, + /* _flags = */ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + /* _format = */ PixelFormat.TRANSLUCENT + ).apply { + gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL + } + } + + fun show() { + if (isShown) { + return + } + + val ctx = binding.root.context + + if (!Settings.canDrawOverlays(ctx)) { + logger.warn("Overlay permission denied. Skipping debugger overlay window.") + + val state = PermissionsHelper.getOverlayPermissionState(ctx) + val message = if (state == PermissionsHelper.OverlayPermissionState.UNSUPPORTED) { + ctx.getString(R.string.permission_overlay_unsupported_hint) + } else { + ctx.getString(R.string.permission_overlay_restricted_settings_hint) + } + Toast.makeText(ctx, message, Toast.LENGTH_LONG).show() + return + } + + try { + windowManager.addView(binding.root, overlayLayoutParams) + isShown = true + } catch (err: Throwable) { + logger.error("Failed to show debugger overlay window", err) + } + } + + fun hide() { + if (!isShown) { + return + } + + try { + windowManager.removeView(binding.root) + } catch (err: Throwable) { + logger.error("Failed to hide debugger overlay window", err) + } finally { + isShown = false + } + } + + fun refreshActions() { + // noinspection NotifyDataSetChanged + binding.actions.adapter?.notifyDataSetChanged() + } + + companion object { + + private val logger = LoggerFactory.getLogger(DebugOverlayManager::class.java) + private const val WM_OVERLAY_TYPE = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + + /** * Create a new [DebugOverlayManager] from the given [Context]. + * + * @param ctx The [Context] to use for creating the [DebugOverlayManager]. + * @return A new [DebugOverlayManager]. + */ + fun create(ctx: Context, displayId: Int = -1): DebugOverlayManager { + val windowContext = run { + if (displayId == -1) { + logger.debug("no display id provided. overlay will be shown on default display") + return@run ctx + } + + logger.debug("trying to get window context for displayId={}", displayId) + val displayManager = DisplayManagerCompat.getInstance(ctx) + val targetDisplay = displayManager.getDisplay(displayId) + val displayContext = + if (targetDisplay != null) ctx.createDisplayContext(targetDisplay) else ctx + + if (isAtLeastR()) { + displayContext.createWindowContext(WM_OVERLAY_TYPE, null) + } else displayContext + } + + // IMPORTANT! + // Wrap the context with a theme, so we could use MaterialButtons! + val context = ContextThemeWrapper(windowContext, R.style.Theme_AndroidIDE) + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val inflater = LayoutInflater.from(context) + + // noinspection InflateParams + val layout = DebuggerActionsWindowBinding.inflate(inflater) + + val actionsRegistry = ActionsRegistry.getInstance() + val debuggerActions = actionsRegistry.getActions(ActionItem.Location.DEBUGGER_ACTIONS) + + val actions = debuggerActions.values.toList() + val adapter = DebuggerActionsOverlayAdapter(actions) + layout.actions.adapter = adapter + + logger.debug("overlay manager created for display {}", displayId) + return DebugOverlayManager( + windowManager = windowManager, + binding = layout, + attachedDisplayId = displayId, + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/services/debug/DebuggerService.kt b/app/src/main/java/com/itsaky/androidide/services/debug/DebuggerService.kt index 5c7860d528..9dcce7fbf2 100644 --- a/app/src/main/java/com/itsaky/androidide/services/debug/DebuggerService.kt +++ b/app/src/main/java/com/itsaky/androidide/services/debug/DebuggerService.kt @@ -35,11 +35,12 @@ class DebuggerService : Service() { companion object { private val logger = LoggerFactory.getLogger(DebuggerService::class.java) + const val EXTRA_DISPLAY_ID = "debugger.overlay.displayId" } private val actionsRegistry = ActionsRegistry.getInstance() private lateinit var actionsList: List - private lateinit var overlayManager: DebugOverlayManager + private var overlayManager: DebugOverlayManager? = null private val binder = Binder() private val serviceScope = CoroutineScope(Dispatchers.Default) @@ -61,7 +62,6 @@ class DebuggerService : Service() { } this.actionsList.forEach(actionsRegistry::registerAction) - this.overlayManager = DebugOverlayManager.create(this) serviceScope.launch { ForegroundAppReceiver.foregroundAppState @@ -103,12 +103,7 @@ class DebuggerService : Service() { targetPackage = null serviceScope.cancelIfActive("DebuggerService is being destroyed") - try { - overlayManager.hide() - } catch (err: Throwable) { - logger.error("Failed to hide debugger overlay", err) - } - + detachOverlay() super.onDestroy() actionsList.forEach(actionsRegistry::unregisterAction) @@ -116,23 +111,55 @@ class DebuggerService : Service() { fun showOverlay() { logger.debug("showOverlay()") - this.overlayManager.show() + this.overlayManager?.show() } fun hideOverlay() { logger.debug("hideOverlay()") - this.overlayManager.hide() + this.overlayManager?.hide() } fun setOverlayVisibility(isShown: Boolean) = if (isShown) showOverlay() else hideOverlay() + fun maybeMoveOverlayToDisplay(displayId: Int) { + if (overlayManager?.attachedDisplayId == displayId) return + + hideOverlay() + createOverlayManagerIfNeeded(displayId) + showOverlay() + } + + private fun createOverlayManagerIfNeeded(displayId: Int) { + if (overlayManager?.attachedDisplayId != displayId) { + hideOverlay() + overlayManager = null + } + + if (overlayManager == null) { + overlayManager = + DebugOverlayManager.create( + ctx = this, + displayId = displayId + ) + } + } + + private fun detachOverlay() { + try { + hideOverlay() + } catch (err: Throwable) { + logger.error("Failed to hide debugger overlay", err) + } + } + fun onConnectionStateUpdated(newState: DebuggerConnectionState) { setOverlayVisibility(newState >= DebuggerConnectionState.ATTACHED) - overlayManager.refreshActions() + overlayManager?.refreshActions() } override fun onBind(intent: Intent?): IBinder { logger.debug("onBind(intent={}): extras={}", intent, intent?.extras) + createOverlayManagerIfNeeded(displayId = intent?.extras?.getInt(EXTRA_DISPLAY_ID, -1) ?: -1) return this.binder }