From 494fe94d866e255198a805bc6dfbd8de075ea1b6 Mon Sep 17 00:00:00 2001 From: Daniel Alome Date: Wed, 2 Sep 2026 07:47:59 +0100 Subject: [PATCH 1/7] style: spotless reformat OverlayLayoutParams, no functional change Editing this file for ADFA-4500 enrols it in the origin/stage ratchet, which reformats it in full. Committed on its own so the behavioural change that follows stays readable. --- .../floating/window/OverlayLayoutParams.kt | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayLayoutParams.kt b/floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayLayoutParams.kt index 213cdea83d..f93b6e031b 100644 --- a/floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayLayoutParams.kt +++ b/floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayLayoutParams.kt @@ -16,7 +16,6 @@ import android.view.WindowManager * focusable so the IME can attach. */ object OverlayLayoutParams { - private val overlayType: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY @@ -25,19 +24,23 @@ object OverlayLayoutParams { WindowManager.LayoutParams.TYPE_PHONE } - fun create(state: FloatingWindowState, focusable: Boolean): WindowManager.LayoutParams = - WindowManager.LayoutParams( - state.bounds.width, - state.bounds.height, - overlayType, - flagsFor(focusable), - PixelFormat.TRANSLUCENT, - ).apply { - gravity = Gravity.TOP or Gravity.START - x = state.bounds.x - y = state.bounds.y - softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE - } + fun create( + state: FloatingWindowState, + focusable: Boolean, + ): WindowManager.LayoutParams = + WindowManager + .LayoutParams( + state.bounds.width, + state.bounds.height, + overlayType, + flagsFor(focusable), + PixelFormat.TRANSLUCENT, + ).apply { + gravity = Gravity.TOP or Gravity.START + x = state.bounds.x + y = state.bounds.y + softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + } fun flagsFor(focusable: Boolean): Int { val common = From 3bbb6f5eddc1c29587b158bb7d27710446d7ab3d Mon Sep 17 00:00:00 2001 From: Daniel Alome Date: Wed, 2 Sep 2026 07:48:34 +0100 Subject: [PATCH 2/7] ADFA-4500: Add PluginWindows for dialogs and toasts from a floating window A plugin fragment undocked into a floating window runs against a window context created for TYPE_APPLICATION_OVERLAY, and the platform requires every window added through it to carry that same type. A Dialog builds a TYPE_APPLICATION window and a Toast a TYPE_TOAST one, so AlertDialog.Builder(requireContext()) .show() and Toast.makeText(requireContext(), ...) both throw IllegalArgumentException the moment the tab is floating. Neither can be corrected by the IDE on the plugin's behalf: Window exposes no theme attribute for its type and hard-casts to WindowManagerImpl, so a getSystemService proxy cannot rewrite it, and a toast is posted by the system against whatever context built it. The platform's own multi-type exemption is limited to a WindowProviderService, a system API. So plugins need an entry point of their own. PluginWindows.showDialog applies the type the dialog's context requires; showToast posts against the application context, which imposes no window type. Both are no-ops while docked, so one call site is correct in either state. ABI change is purely additive; apiCheck passes. --- docs/PLUGIN_API_CHANGELOG.md | 16 ++++ plugin-api/api/plugin-api.api | 9 +++ .../androidide/plugins/base/PluginWindows.kt | 73 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/PluginWindows.kt diff --git a/docs/PLUGIN_API_CHANGELOG.md b/docs/PLUGIN_API_CHANGELOG.md index 744ea89ed8..b1b3e66537 100644 --- a/docs/PLUGIN_API_CHANGELOG.md +++ b/docs/PLUGIN_API_CHANGELOG.md @@ -36,6 +36,22 @@ milestone. **[verified]** = read from the checked-in ABI dump. **[reconstructed] = diffed from `plugin-api/src` history (predates the dump; symbol-accurate). ### 26.36 — unreleased +- **added — Dialogs and toasts from a floating window** _(ADFA-4500)_ + Show a dialog or a toast from a plugin tab the user has undocked into a floating + window. An undocked fragment runs against a window context created for + `TYPE_APPLICATION_OVERLAY`, and the platform requires every window added through it + to carry that same type. A `Dialog` builds a `TYPE_APPLICATION` window and a `Toast` + a `TYPE_TOAST` one, so both `AlertDialog.Builder(requireContext()).show()` and + `Toast.makeText(requireContext(), ...)` throw `IllegalArgumentException` once the tab + is floating — the toast after any work preceding it has already run. + `PluginWindows.showDialog(Dialog)` applies the window type the dialog's context + requires and shows it (`prepareDialog` does so without showing; build the dialog with + `create()` rather than showing it from its builder). + `PluginWindows.showToast(Context, CharSequence, Int)` posts the toast against the + application context, which imposes no window type. Both are no-ops while the plugin is + docked, so one call site is correct in either state. Neither can be applied by the IDE + on a plugin's behalf: `Window` has no theme attribute for its type, and a toast is + posted by the system against whatever context built it. - **added — File-targeted editor save** _(ADFA-5259)_ Save a named file's open buffer and find out whether the bytes actually landed. `saveCurrentFile` follows whichever tab the user has focused and returns as soon diff --git a/plugin-api/api/plugin-api.api b/plugin-api/api/plugin-api.api index 4ec832d1f9..5b376a5dce 100644 --- a/plugin-api/api/plugin-api.api +++ b/plugin-api/api/plugin-api.api @@ -141,6 +141,15 @@ public final class com/itsaky/androidide/plugins/base/PluginFragmentHelper { public static final fun setOnPluginInflationError (Lkotlin/jvm/functions/Function2;)V } +public final class com/itsaky/androidide/plugins/base/PluginWindows { + public static final field INSTANCE Lcom/itsaky/androidide/plugins/base/PluginWindows; + public static final fun prepareDialog (Landroid/app/Dialog;)V + public static final fun showDialog (Landroid/app/Dialog;)V + public static final fun showToast (Landroid/content/Context;Ljava/lang/CharSequence;)V + public static final fun showToast (Landroid/content/Context;Ljava/lang/CharSequence;I)V + public static synthetic fun showToast$default (Landroid/content/Context;Ljava/lang/CharSequence;IILjava/lang/Object;)V +} + public final class com/itsaky/androidide/plugins/base/SafePluginLayoutInflater : android/view/LayoutInflater { public static final field Companion Lcom/itsaky/androidide/plugins/base/SafePluginLayoutInflater$Companion; public synthetic fun (Landroid/view/LayoutInflater;Landroid/content/Context;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/PluginWindows.kt b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/PluginWindows.kt new file mode 100644 index 0000000000..759d83d8d9 --- /dev/null +++ b/plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/PluginWindows.kt @@ -0,0 +1,73 @@ +package com.itsaky.androidide.plugins.base + +import android.app.Activity +import android.app.Dialog +import android.content.Context +import android.content.ContextWrapper +import android.provider.Settings +import android.view.WindowManager +import android.widget.Toast + +/** + * Shows plugin dialogs + * + * A plugin fragment undocked into a floating window runs against a window context created for + * [WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY]. The platform requires every window added + * through such a context to carry that same type and rejects anything else, so both + * `AlertDialog.Builder(requireContext()).show()` (a `TYPE_APPLICATION` window) and + * `Toast.makeText(requireContext(), ...)` (a `TYPE_TOAST` window) throw `IllegalArgumentException` + * as soon as the plugin is undocked. Neither can be corrected by the IDE on the plugin's behalf -- + * `Window` exposes no theme attribute for its type, and a toast is posted by the system against + * whatever context built it -- so route both through here. + * + * Both entry points are no-ops for a docked plugin, whose context is the IDE activity, so a single + * call site is correct in either state. + */ +object PluginWindows { + /** Applies the window type [dialog]'s context requires, then shows it. */ + @JvmStatic + fun showDialog(dialog: Dialog) { + prepareDialog(dialog) + dialog.show() + } + + /** + * Retypes [dialog]'s window as an overlay when its context is not activity-backed, so that it + * can be shown from a floating window. Build the dialog with `create()` rather than showing it + * from its builder, then pass it here. + */ + @JvmStatic + fun prepareDialog(dialog: Dialog) { + val context = dialog.context + if (context.findActivity() != null || !Settings.canDrawOverlays(context)) { + return + } + dialog.window?.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY) + } + + /** + *A toast's window is added by the system + * against the context that built it, and its type is fixed, so this builds it against the + * application context -- which imposes no window type of its own. + */ + @JvmStatic + @JvmOverloads + fun showToast( + context: Context, + text: CharSequence, + duration: Int = Toast.LENGTH_SHORT, + ) { + Toast.makeText(context.applicationContext, text, duration).show() + } + + private fun Context.findActivity(): Activity? { + var current: Context? = this + while (current is ContextWrapper) { + if (current is Activity) { + return current + } + current = current.baseContext + } + return null + } +} From 7489a3b2bf0d94351fc06c841b6ca4521cad22dc Mon Sep 17 00:00:00 2001 From: Daniel Alome Date: Wed, 2 Sep 2026 07:49:04 +0100 Subject: [PATCH 3/7] ADFA-4500: Show plugin crash dialogs above floating windows A floating window is TYPE_APPLICATION_OVERLAY, which the platform always stacks above an activity's own windows. The plugin crash dialog was an ordinary activity dialog, so it rendered behind the floating window, and because an overlay is a separate window its modality did not extend there: tapping the overlay never surfaced the dialog. The reporter had to move, minimise or dock the floating window to reach it. OverlayDialogs raises a dialog to the same window type when any floating window is open, leaving an ordinary activity dialog untouched when nothing is floating. The platform attaches no app token to a system-type window (Window.adjustLayoutParamsForSubWindow: "the life cycles should be independent"), so a raised dialog outlives the activity; EditorHandlerActivity tracks the ones it raises and dismisses them in onDestroy. Applied to both crash dialogs, the summary and the log view. Two sibling dialogs in this activity have the same problem and are deliberately left alone: the unsaved-files confirmation and ADFA-4501's close-project dialog. overlayType becomes public so the window type has a single definition. --- .../editor/EditorHandlerActivity.kt | 48 +++++++++++++------ .../floating/window/OverlayDialogs.kt | 32 +++++++++++++ .../floating/window/OverlayLayoutParams.kt | 2 +- 3 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayDialogs.kt diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt index c3f187e2dd..78ba77f4d9 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt @@ -17,6 +17,7 @@ package com.itsaky.androidide.activities.editor +import android.app.Dialog import android.content.ClipData import android.content.ClipboardManager import android.content.Intent @@ -68,6 +69,7 @@ import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent import com.itsaky.androidide.eventbus.events.file.FileRenameEvent import com.itsaky.androidide.eventbus.events.plugin.PluginCrashedEvent import com.itsaky.androidide.eventbus.events.preferences.PreferenceChangeEvent +import com.itsaky.androidide.floating.window.OverlayDialogs import com.itsaky.androidide.fragments.sidebar.EditorSidebarFragment import com.itsaky.androidide.idetooltips.TooltipManager import com.itsaky.androidide.idetooltips.TooltipTag @@ -220,6 +222,8 @@ open class EditorHandlerActivity : pluginEditorProvider = null } + private val crashDialogsAboveFloatingWindows = mutableListOf() + private val floatingTabController by lazy { com.itsaky.androidide.editor.floating .IdeFloatingTabController(this) @@ -329,6 +333,7 @@ open class EditorHandlerActivity : override fun onDestroy() { super.onDestroy() + dismissCrashDialogsAboveFloatingWindows() ActionContextProvider.clearActivity(this) } @@ -1493,6 +1498,17 @@ open class EditorHandlerActivity : } } + private fun showAboveFloatingWindows(dialog: Dialog) { + crashDialogsAboveFloatingWindows.add(dialog) + dialog.setOnDismissListener { crashDialogsAboveFloatingWindows.remove(dialog) } + OverlayDialogs.show(dialog) + } + + private fun dismissCrashDialogsAboveFloatingWindows() { + crashDialogsAboveFloatingWindows.toList().forEach { it.dismiss() } + crashDialogsAboveFloatingWindows.clear() + } + private fun showPluginCrashDialog(event: PluginCrashedEvent) { val dialogView = layoutInflater.inflate(R.layout.dialog_plugin_crash, null) dialogView.findViewById(R.id.plugin_crash_message).text = @@ -1514,7 +1530,7 @@ open class EditorHandlerActivity : } } - builder.show() + showAboveFloatingWindows(builder.create()) dialogView.findViewById(R.id.plugin_crash_view_logs).setOnClickListener { showPluginCrashLogDialog(event) @@ -1522,20 +1538,22 @@ open class EditorHandlerActivity : } private fun showPluginCrashLogDialog(event: PluginCrashedEvent) { - newMaterialDialogBuilder(this) - .setTitle(getString(string.title_plugin_crash_log, event.pluginName)) - .setMessage(event.stackTrace) - .setPositiveButton(string.close, null) - .setNeutralButton(string.copy) { _, _ -> - val clipboard = getSystemService(ClipboardManager::class.java) - clipboard?.setPrimaryClip( - ClipData.newPlainText( - getString(string.title_plugin_crash_log, event.pluginName), - event.stackTrace, - ), - ) - flashSuccess(string.msg_crash_log_copied) - }.show() + showAboveFloatingWindows( + newMaterialDialogBuilder(this) + .setTitle(getString(string.title_plugin_crash_log, event.pluginName)) + .setMessage(event.stackTrace) + .setPositiveButton(string.close, null) + .setNeutralButton(string.copy) { _, _ -> + val clipboard = getSystemService(ClipboardManager::class.java) + clipboard?.setPrimaryClip( + ClipData.newPlainText( + getString(string.title_plugin_crash_log, event.pluginName), + event.stackTrace, + ), + ) + flashSuccess(string.msg_crash_log_copied) + }.create(), + ) } private fun tearDownDisabledPluginContributions(pluginId: String) { diff --git a/floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayDialogs.kt b/floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayDialogs.kt new file mode 100644 index 0000000000..c317302d3f --- /dev/null +++ b/floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayDialogs.kt @@ -0,0 +1,32 @@ +package com.itsaky.androidide.floating.window + +import android.app.Dialog +import com.itsaky.androidide.floating.model.DockingManager +import com.itsaky.androidide.floating.permission.OverlayPermission + +/** + * Shows a host [Dialog] above any floating overlay windows. + * + * Floating windows are [android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY], which + * the platform always stacks above an activity's own windows. An activity dialog therefore renders + * *behind* them, and because an overlay is a separate window the dialog's modality does not extend + * to it: tapping the overlay never surfaces the dialog. Raising the dialog to the same window type + * puts it back on top, where its modality is visible. + * + * The platform attaches no app token to a system-type window, so a raised dialog outlives the + * activity that created it. Callers must dismiss it themselves when the activity goes away. + */ +object OverlayDialogs { + /** + * Shows [dialog], raising it above the floating windows when any are open. Leaves an ordinary + * activity dialog untouched when nothing is floating. + */ + fun show(dialog: Dialog) { + if (DockingManager.windows.value.isNotEmpty() && + OverlayPermission.canDrawOverlays(dialog.context) + ) { + dialog.window?.setType(OverlayLayoutParams.overlayType) + } + dialog.show() + } +} diff --git a/floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayLayoutParams.kt b/floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayLayoutParams.kt index f93b6e031b..2f68b799c9 100644 --- a/floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayLayoutParams.kt +++ b/floating-window/src/main/java/com/itsaky/androidide/floating/window/OverlayLayoutParams.kt @@ -16,7 +16,7 @@ import android.view.WindowManager * focusable so the IME can attach. */ object OverlayLayoutParams { - private val overlayType: Int = + val overlayType: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } else { From 43b6500ab2cc2682aa6dcfedf430d18a2ea58701 Mon Sep 17 00:00:00 2001 From: Daniel Alome Date: Wed, 2 Sep 2026 07:49:36 +0100 Subject: [PATCH 4/7] ADFA-4500: Restore Material widget inflation in floating windows An activity installs AppCompat's view factory on its LayoutInflater, and that is what turns an unqualified