1+ package org.operatorfoundation.transmission
2+
3+ import android.content.BroadcastReceiver
4+ import android.content.Context
5+ import android.content.Intent
6+ import android.content.IntentFilter
7+ import android.hardware.usb.UsbManager
8+ import kotlinx.coroutines.flow.StateFlow
9+ import timber.log.Timber
10+
11+ /* *
12+ * Application-scoped monitor for USB serial device availability.
13+ *
14+ * Wraps [SerialConnectionFactory] and self-registers a USB broadcast receiver
15+ * using application context, so attach/detach events are handled for the full
16+ * app lifetime without requiring any Activity involvement.
17+ *
18+ * Instantiate once in Application.onCreate() and hold as a property.
19+ * Observe [connectionState] to react to connection changes.
20+ *
21+ * Usage:
22+ * ```
23+ * // In Application.onCreate():
24+ * serialDeviceMonitor = SerialDeviceMonitor(applicationContext)
25+ *
26+ * // Anywhere that needs connection state:
27+ * serialDeviceMonitor.connectionState.collect { state -> ... }
28+ * ```
29+ */
30+ class SerialDeviceMonitor (private val context : Context )
31+ {
32+ private val factory = SerialConnectionFactory (context)
33+
34+ /* * Current serial connection state. Observe this to react to connect/disconnect. */
35+ val connectionState: StateFlow <SerialConnectionFactory .ConnectionState > = factory.connectionState
36+
37+ /* * True when a serial connection is established. Convenience derived from [connectionState]. */
38+ val isConnected: Boolean
39+ get() = connectionState.value is SerialConnectionFactory .ConnectionState .Connected
40+
41+ // ==================== USB Broadcast Receiver ====================
42+
43+ private val usbReceiver = object : BroadcastReceiver ()
44+ {
45+ override fun onReceive (context : Context , intent : Intent )
46+ {
47+ when (intent.action)
48+ {
49+ UsbManager .ACTION_USB_DEVICE_ATTACHED ->
50+ {
51+ Timber .d(" SerialDeviceMonitor: USB device attached" )
52+ onDeviceAttached()
53+ }
54+
55+ UsbManager .ACTION_USB_DEVICE_DETACHED ->
56+ {
57+ Timber .d(" SerialDeviceMonitor: USB device detached" )
58+ factory.onDeviceDetached()
59+ }
60+ }
61+ }
62+ }
63+
64+ init
65+ {
66+ // Register receiver for the app's lifetime using application context.
67+ // No need to unregister — this lives as long as the app does.
68+ val filter = IntentFilter ().apply {
69+ addAction(UsbManager .ACTION_USB_DEVICE_ATTACHED )
70+ addAction(UsbManager .ACTION_USB_DEVICE_DETACHED )
71+ }
72+
73+ context.registerReceiver(usbReceiver, filter, Context .RECEIVER_NOT_EXPORTED )
74+ Timber .d(" SerialDeviceMonitor: USB receiver registered" )
75+
76+ // Check for already-connected devices on startup
77+ // (device may have been attached before the app launched)
78+ onDeviceAttached()
79+ }
80+
81+ // ==================== Private ====================
82+
83+ /* *
84+ * Called on USB attach or startup. Finds available serial devices
85+ * and initiates connection to the first one found, if any.
86+ *
87+ * No-ops if a connection is already established or in progress.
88+ */
89+ private fun onDeviceAttached ()
90+ {
91+ // Don't attempt if already connected or connecting
92+ val current = connectionState.value
93+ if (current is SerialConnectionFactory .ConnectionState .Connected ||
94+ current is SerialConnectionFactory .ConnectionState .Connecting ||
95+ current is SerialConnectionFactory .ConnectionState .RequestingPermission )
96+ {
97+ Timber .d(" SerialDeviceMonitor: connection already active, ignoring attach" )
98+ return
99+ }
100+
101+ val devices = factory.findAvailableDevices()
102+ if (devices.isEmpty())
103+ {
104+ Timber .d(" SerialDeviceMonitor: no serial devices found" )
105+ return
106+ }
107+
108+ Timber .d(" SerialDeviceMonitor: found ${devices.size} device(s), connecting to first" )
109+ factory.createConnection(devices.first().device)
110+ }
111+ }
0 commit comments