Files
ux/android/src/main/kotlin/io/swipelab/ux/WindowPlugin.kt
agra 27cfc87def notifications + window: add Android native plugins
`ux/notifications/events` and `ux/window/events` only had macOS stream
handlers, so on Android/iOS the unconditional Dart subscription threw
MissingPluginException at startup (EventChannel reports activation
failures straight to FlutterError.onError, bypassing the `onError:`
callback).

- Gate each Dart event-channel subscription to platforms that register a
  native handler (`defaultTargetPlatform`), silencing iOS.
- `WindowPlugin`: report app foreground/background as host focus via
  `ProcessLifecycleOwner` ON_START/ON_STOP, so a backgrounded-but-alive
  process reports `focused = false`.
- `NotificationsPlugin`: local notifications (show/cancel by thread/all),
  POST_NOTIFICATIONS request on 13+, and tap routing back over the event
  channel — a tap that cold-starts the process is buffered until Dart
  subscribes.
- Regression tests for the subscription gate plus contract tests for the
  method/event payloads.
2026-05-30 13:39:49 +03:00

58 lines
2.2 KiB
Kotlin

package io.swipelab.ux
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.ProcessLifecycleOwner
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
/// `ux/window/events`. Reports app foreground / background as host-focus
/// state — emits {"type":"focus","focused":Bool} on `ProcessLifecycleOwner`
/// ON_START / ON_STOP. The Apple counterpart is `WindowPlugin.swift`
/// (NSApplication active/resign); together they back Dart's `XWindow`.
class WindowPlugin : NativePlugin, EventChannel.StreamHandler {
private var eventChannel: EventChannel? = null
private var eventSink: EventChannel.EventSink? = null
/// ProcessLifecycleOwner delivers ON_START/ON_STOP on the main thread,
/// which is also where EventSink.success must run — no Handler bounce.
private val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> emitFocus(true)
Lifecycle.Event.ON_STOP -> emitFocus(false)
else -> Unit
}
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
val ec = EventChannel(binding.binaryMessenger, "ux/window/events")
ec.setStreamHandler(this)
eventChannel = ec
ProcessLifecycleOwner.get().lifecycle.addObserver(observer)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
ProcessLifecycleOwner.get().lifecycle.removeObserver(observer)
eventChannel?.setStreamHandler(null)
eventChannel = null
eventSink = null
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
// Seed the current state so a late subscriber gets the right value
// immediately instead of waiting for the next ON_START / ON_STOP.
val foreground = ProcessLifecycleOwner.get().lifecycle.currentState
.isAtLeast(Lifecycle.State.STARTED)
emitFocus(foreground)
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
private fun emitFocus(focused: Boolean) {
eventSink?.success(mapOf("type" to "focus", "focused" to focused))
}
}