@@ -8,11 +8,13 @@ import android.content.pm.PackageManager
88import android.os.Build
99import android.os.Bundle
1010import android.provider.Settings
11+ import android.view.KeyEvent
1112import androidx.activity.ComponentActivity
1213import androidx.activity.compose.setContent
1314import androidx.activity.enableEdgeToEdge
1415import androidx.activity.result.contract.ActivityResultContracts
1516import androidx.compose.foundation.layout.safeDrawingPadding
17+ import androidx.compose.runtime.CompositionLocalProvider
1618import androidx.compose.runtime.LaunchedEffect
1719import androidx.compose.runtime.collectAsState
1820import androidx.compose.runtime.getValue
@@ -29,6 +31,10 @@ import com.gios.lightpods.bt.MediaControls
2931import com.gios.lightpods.bt.PodsConnector
3032import com.gios.lightpods.data.PodsRepository
3133import com.gios.lightpods.data.isStale
34+ import com.gios.lightpods.hw.LightKey
35+ import com.gios.lightpods.hw.LightKeys
36+ import com.gios.lightpods.hw.LocalWheelBus
37+ import com.gios.lightpods.hw.WheelBus
3238import com.gios.lightpods.service.PodsService
3339import com.gios.lightpods.ui.DebugScreen
3440import com.gios.lightpods.ui.HomeScreen
@@ -40,6 +46,9 @@ class MainActivity : ComponentActivity() {
4046
4147 private var permissionsGranted by mutableStateOf(false )
4248
49+ /* * Wheel notches on their way to whichever screen is up. */
50+ private val wheel = WheelBus ()
51+
4352 private val requestPermissions =
4453 registerForActivityResult(ActivityResultContracts .RequestMultiplePermissions ()) {
4554 permissionsGranted = hasPermissions()
@@ -49,6 +58,29 @@ class MainActivity : ComponentActivity() {
4958 }
5059 }
5160
61+ /* *
62+ * Every hardware key arrives here first — `DecorView` hands the event to the window
63+ * callback before it walks the view hierarchy — so a notch reaches the screen whatever
64+ * happens to hold focus.
65+ *
66+ * Only the turns. The wheel click and the camera button belong to LightControl, which
67+ * owns them phone-wide and passes bare turns through on purpose.
68+ */
69+ override fun dispatchKeyEvent (event : KeyEvent ): Boolean {
70+ when (LightKeys .of(event)) {
71+ LightKey .WheelUp -> {
72+ if (event.action == KeyEvent .ACTION_DOWN ) wheel.send(1 )
73+ return true
74+ }
75+ LightKey .WheelDown -> {
76+ if (event.action == KeyEvent .ACTION_DOWN ) wheel.send(- 1 )
77+ return true
78+ }
79+ else -> Unit
80+ }
81+ return super .dispatchKeyEvent(event)
82+ }
83+
5284 override fun onCreate (savedInstanceState : Bundle ? ) {
5385 super .onCreate(savedInstanceState)
5486 // Targeting SDK 35 forces edge to edge, so the theme's bar colours stop
@@ -60,117 +92,119 @@ class MainActivity : ComponentActivity() {
6092 val connector = PodsConnector (this )
6193
6294 setContent {
63- LightPodsTheme {
64- val scope = rememberCoroutineScope()
65- val view by PodsRepository .view.collectAsState()
66- val candidates by PodsRepository .candidates.collectAsState()
67- val connected by PodsRepository .connected.collectAsState()
68- val connectMessage by PodsRepository .connectResult.collectAsState()
69-
70- var connecting by remember { mutableStateOf(false ) }
71- var showDebug by remember { mutableStateOf(false ) }
72- var probing by remember { mutableStateOf(false ) }
73- var probeResult by remember { mutableStateOf<String ?>(null ) }
74- var tick by remember { mutableLongStateOf(System .currentTimeMillis()) }
75- var playing by remember { mutableStateOf(false ) }
76- var volume by remember { mutableFloatStateOf(0f ) }
77-
78- // Going out of range, switching Bluetooth off, and pausing playback in
79- // another app all produce silence rather than an event, so the screen
80- // needs its own heartbeat to notice them.
81- LaunchedEffect (Unit ) {
82- while (true ) {
83- tick = System .currentTimeMillis()
84- playing = media.isPlaying
85- volume = media.volume()
86- delay(2_000 )
95+ CompositionLocalProvider (LocalWheelBus provides wheel) {
96+ LightPodsTheme {
97+ val scope = rememberCoroutineScope()
98+ val view by PodsRepository .view.collectAsState()
99+ val candidates by PodsRepository .candidates.collectAsState()
100+ val connected by PodsRepository .connected.collectAsState()
101+ val connectMessage by PodsRepository .connectResult.collectAsState()
102+
103+ var connecting by remember { mutableStateOf(false ) }
104+ var showDebug by remember { mutableStateOf(false ) }
105+ var probing by remember { mutableStateOf(false ) }
106+ var probeResult by remember { mutableStateOf<String ?>(null ) }
107+ var tick by remember { mutableLongStateOf(System .currentTimeMillis()) }
108+ var playing by remember { mutableStateOf(false ) }
109+ var volume by remember { mutableFloatStateOf(0f ) }
110+
111+ // Going out of range, switching Bluetooth off, and pausing playback in
112+ // another app all produce silence rather than an event, so the screen
113+ // needs its own heartbeat to notice them.
114+ LaunchedEffect (Unit ) {
115+ while (true ) {
116+ tick = System .currentTimeMillis()
117+ playing = media.isPlaying
118+ volume = media.volume()
119+ delay(2_000 )
120+ }
87121 }
88- }
89122
90- val stale = remember(view, tick) { view?.isStale(tick) == true }
91- val bluetoothOn = remember(tick) { isBluetoothOn() }
123+ val stale = remember(view, tick) { view?.isStale(tick) == true }
124+ val bluetoothOn = remember(tick) { isBluetoothOn() }
125+
126+ if (showDebug) {
127+ DebugScreen (
128+ view = view,
129+ candidates = candidates,
130+ probeResult = probeResult,
131+ probing = probing,
132+ onProbe = {
133+ probing = true
134+ probeResult = null
135+ scope.launch {
136+ val adapter = connector.adapter
137+ val device = connector.targetDevice()
138+ probeResult = when {
139+ adapter == null -> " no Bluetooth adapter"
140+ device == null -> " no bonded audio device to probe"
141+ else -> AapProbe .run (adapter, device).detail
142+ }
143+ probing = false
144+ }
145+ },
146+ onBack = { showDebug = false },
147+ )
148+ return @LightPodsTheme
149+ }
92150
93- if (showDebug) {
94- DebugScreen (
151+ HomeScreen (
152+ modifier = Modifier .safeDrawingPadding(),
95153 view = view,
96- candidates = candidates,
97- probeResult = probeResult,
98- probing = probing,
99- onProbe = {
100- probing = true
101- probeResult = null
154+ stale = stale,
155+ bluetoothOn = bluetoothOn,
156+ permissionsGranted = permissionsGranted,
157+ connected = connected,
158+ playing = playing,
159+ volume = volume,
160+ connecting = connecting,
161+ connectMessage = connectMessage,
162+ onConnect = {
163+ connecting = true
164+ PodsRepository .setConnectResult(null )
102165 scope.launch {
103- val adapter = connector.adapter
104- val device = connector.targetDevice()
105- probeResult = when {
106- adapter == null -> " no Bluetooth adapter"
107- device == null -> " no bonded audio device to probe"
108- else -> AapProbe .run (adapter, device).detail
166+ val result = connector.connect()
167+ connecting = false
168+ when (result) {
169+ is PodsConnector .Result .Connected -> {
170+ PodsRepository .setConnected(true )
171+ PodsRepository .setConnectResult(
172+ " ${result.device} — ${result.via} " ,
173+ )
174+ }
175+
176+ PodsConnector .Result .NoDevice ->
177+ PodsRepository .setConnectResult(" Pair your earbuds first" )
178+
179+ PodsConnector .Result .BluetoothOff ->
180+ PodsRepository .setConnectResult(" Bluetooth is off" )
181+
182+ is PodsConnector .Result .Failed -> {
183+ PodsRepository .setConnectResult(" Could not connect" )
184+ openBluetoothSettings()
185+ }
109186 }
110- probing = false
111187 }
112188 },
113- onBack = { showDebug = false },
189+ onPlayPause = {
190+ media.playPause()
191+ playing = ! playing
192+ },
193+ onPrevious = media::previous,
194+ onNext = media::next,
195+ onVolumeUp = {
196+ media.volumeUp()
197+ volume = media.volume()
198+ },
199+ onVolumeDown = {
200+ media.volumeDown()
201+ volume = media.volume()
202+ },
203+ onGrantPermissions = { requestPermissions.launch(requiredPermissions()) },
204+ onOpenSettings = ::openBluetoothSettings,
205+ onDebug = { showDebug = true },
114206 )
115- return @LightPodsTheme
116207 }
117-
118- HomeScreen (
119- modifier = Modifier .safeDrawingPadding(),
120- view = view,
121- stale = stale,
122- bluetoothOn = bluetoothOn,
123- permissionsGranted = permissionsGranted,
124- connected = connected,
125- playing = playing,
126- volume = volume,
127- connecting = connecting,
128- connectMessage = connectMessage,
129- onConnect = {
130- connecting = true
131- PodsRepository .setConnectResult(null )
132- scope.launch {
133- val result = connector.connect()
134- connecting = false
135- when (result) {
136- is PodsConnector .Result .Connected -> {
137- PodsRepository .setConnected(true )
138- PodsRepository .setConnectResult(
139- " ${result.device} — ${result.via} " ,
140- )
141- }
142-
143- PodsConnector .Result .NoDevice ->
144- PodsRepository .setConnectResult(" Pair your earbuds first" )
145-
146- PodsConnector .Result .BluetoothOff ->
147- PodsRepository .setConnectResult(" Bluetooth is off" )
148-
149- is PodsConnector .Result .Failed -> {
150- PodsRepository .setConnectResult(" Could not connect" )
151- openBluetoothSettings()
152- }
153- }
154- }
155- },
156- onPlayPause = {
157- media.playPause()
158- playing = ! playing
159- },
160- onPrevious = media::previous,
161- onNext = media::next,
162- onVolumeUp = {
163- media.volumeUp()
164- volume = media.volume()
165- },
166- onVolumeDown = {
167- media.volumeDown()
168- volume = media.volume()
169- },
170- onGrantPermissions = { requestPermissions.launch(requiredPermissions()) },
171- onOpenSettings = ::openBluetoothSettings,
172- onDebug = { showDebug = true },
173- )
174208 }
175209 }
176210 }
0 commit comments