Skip to content

Commit 544d0f3

Browse files
eriedclaude
andcommitted
Watch buttons: fix tap crash (VIBRATE permission); fire when offline
Tap / long-press on the new on-screen buttons crashed the watch with a RemoteException from VibratorManagerService — the wear manifest was missing `android.permission.VIBRATE`. Added. Also: - Switched the gesture handler from combinedClickable to pointerInput + detectTapGestures (the foundation API was misbehaving on Wear's Compose stack). - Dropped the `enabled = live` gate so the watch buttons now also fire when no wheel is connected — media keys, voice report, and trip recording all work without a wheel; wheel-only actions silently no-op on the phone side. P6 motor temp: parser now reads body[32] (verified across labelled captures: 29 / 34 / 33 °C wheel-display vs 31.7 / 33.3 / 32.8 °C from this byte). body[31] was off by 5 °C in warm samples. Bumps to 0.3.2-p6preview18. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 49bba52 commit 544d0f3

5 files changed

Lines changed: 56 additions & 48 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ android {
2727
applicationId = "com.eried.eucplanet"
2828
minSdk = 29
2929
targetSdk = 35
30-
versionCode = 26
31-
versionName = "0.3.2-p6preview17"
30+
versionCode = 27
31+
versionName = "0.3.2-p6preview18"
3232

3333
val buildStamp = SimpleDateFormat("yyMMdd.HHmm")
3434
.apply { timeZone = TimeZone.getTimeZone("UTC") }

app/src/main/java/com/eried/eucplanet/ble/InMotionV2Parser.kt

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -243,13 +243,16 @@ object InMotionV2Parser {
243243
ByteUtils.getUint32LE(data, 58) / 100f
244244
} else 0f
245245

246-
// Temperatures: MOS / motor / driver-board sit at body offsets 30/31/32
247-
// in the realtime block. Decoded as `°F = byte − 126` (verified against
248-
// a labelled capture where displayed 72/79/79 °F mapped to bytes 0xc6
249-
// /0xcd/0xcd, then again across a warming-up ride where the values
250-
// climbed in lockstep frame-by-frame). The dashboard's TEMP pill
251-
// tracks the MOTOR specifically, so we surface it as the only entry
252-
// and let `maxTemperature` follow it.
246+
// Temperatures: three sensor bytes at body offsets 30/31/32, decoded
247+
// as `°F = byte − 126`. Labelled-capture cross-check (rider noted
248+
// motor reading on the wheel UI vs the bytes flowing in):
249+
// wheel motor 29°C -> body[30]=27.8 body[31]=26.1 body[32]=31.7
250+
// wheel motor 34°C -> body[30]=28.9 body[31]=28.9 body[32]=33.3
251+
// wheel motor 33°C -> body[30]=28.9 body[31]=28.9 body[32]=32.8
252+
// body[32] tracks motor within ~0.2-0.7°C in the warm samples; the
253+
// other two are nearly identical and noticeably cooler — almost
254+
// certainly the two battery packs' MOS sensors. We surface body[32]
255+
// as motor and keep the others in `temperatures` for future UI.
253256
fun decodeTempC(off: Int): Float? {
254257
if (off >= data.size) return null
255258
val v = data[off].toInt() and 0xFF
@@ -260,14 +263,10 @@ object InMotionV2Parser {
260263
if (f !in 32..248) return null
261264
return (f - 32) * 5f / 9f
262265
}
263-
val motorC = decodeTempC(31)
264-
// We still keep MOS and driver-board accessible in `temperatures`
265-
// for the metric detail screen / future UI, but `maxTemperature`
266-
// tracks motor only so the dashboard pill matches the InMotion
267-
// app's headline reading.
268-
val mosC = decodeTempC(30)
269-
val driverC = decodeTempC(32)
270-
val temps = listOfNotNull(motorC, mosC, driverC)
266+
val motorC = decodeTempC(32)
267+
val mosAC = decodeTempC(30)
268+
val mosBC = decodeTempC(31)
269+
val temps = listOfNotNull(motorC, mosAC, mosBC)
271270

272271
// Headlight state isn't reliably reported in the realtime stream on
273272
// user-facing firmware. preview4's "byte[84] bit 1" rule worked in

wear/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ android {
2222
applicationId = "com.eried.eucplanet"
2323
minSdk = 30
2424
targetSdk = 35
25-
versionCode = 26
26-
versionName = "0.3.2-p6preview17"
25+
versionCode = 27
26+
versionName = "0.3.2-p6preview18"
2727
}
2828

2929
signingConfigs {

wear/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
<uses-feature android:name="android.hardware.type.watch" />
55

6+
<!-- Briefly vibrate the watch on a button-bound action firing, when the
7+
user enables `watchHapticOnAction`. Also used by the `/euc/vibrate`
8+
hint path so phone-side alarm rules can vibrate the watch. -->
9+
<uses-permission android:name="android.permission.VIBRATE" />
10+
611
<application
712
android:allowBackup="false"
813
android:icon="@mipmap/ic_launcher"

wear/src/main/java/com/eried/eucplanet/wear/ui/WatchApp.kt

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ import android.os.VibratorManager
1111
import android.widget.Toast
1212
import androidx.compose.foundation.Canvas
1313
import androidx.compose.foundation.background
14-
import androidx.compose.foundation.combinedClickable
15-
import androidx.compose.foundation.ExperimentalFoundationApi
16-
import androidx.compose.foundation.interaction.MutableInteractionSource
14+
import androidx.compose.foundation.gestures.detectTapGestures
15+
import androidx.compose.ui.input.pointer.pointerInput
1716
import androidx.compose.foundation.layout.Arrangement
1817
import androidx.compose.foundation.layout.offset
1918
import androidx.compose.foundation.layout.BoxWithConstraints
@@ -468,7 +467,6 @@ private fun ActionRow(state: WatchState, accent: Color, buttonSize: Dp, iconSize
468467
* If both click and hold are NONE, the button isn't drawn — the row collapses
469468
* so the user can hide a button entirely if they want a single-button layout.
470469
*/
471-
@OptIn(ExperimentalFoundationApi::class)
472470
@Composable
473471
private fun ConfigurableActionButton(
474472
context: Context,
@@ -511,36 +509,42 @@ private fun ConfigurableActionButton(
511509
else -> contentColor
512510
}
513511

512+
// pointerInput + detectTapGestures is the battle-tested combo for tap +
513+
// long-press on Compose. The earlier `combinedClickable` setup crashed on
514+
// Wear emulator (likely interactionSource/indication mismatch in the
515+
// current Compose foundation version on Wear).
514516
Box(
515517
modifier = Modifier
516518
.size(buttonSize)
517519
.clip(CircleShape)
518520
.background(backgroundColor)
519-
.combinedClickable(
520-
interactionSource = remember { MutableInteractionSource() },
521-
indication = null,
522-
enabled = live,
523-
onClick = {
524-
if (clickAction == "NONE" && holdAction != "NONE") {
525-
// Tap on a hold-only button: hint the user that the
526-
// bound action needs a long press, instead of doing
527-
// nothing (which looks broken).
528-
val label = labelForAction(context, holdAction) ?: holdAction
529-
Toast.makeText(
530-
context,
531-
context.getString(R.string.watch_action_long_press_hint, label),
532-
Toast.LENGTH_SHORT
533-
).show()
534-
} else {
535-
fireAction(context, state, clickAction, showToast = false)
536-
}
537-
},
538-
onLongClick = if (holdAction != "NONE") {
539-
{
540-
fireAction(context, state, holdAction, showToast = true)
541-
}
542-
} else null
543-
),
521+
.pointerInput(clickAction, holdAction, state.lightOn) {
522+
// Fire regardless of `live`: media keys, trip recording, and
523+
// voice announce all work without a connected wheel. The
524+
// wheel-dependent actions (Horn/Light/Lock/Legal) silently
525+
// no-op on the phone side when there's nothing to talk to,
526+
// so blocking the click here just makes the watch feel
527+
// unresponsive.
528+
detectTapGestures(
529+
onTap = {
530+
if (clickAction == "NONE" && holdAction != "NONE") {
531+
val label = labelForAction(context, holdAction) ?: holdAction
532+
Toast.makeText(
533+
context,
534+
context.getString(R.string.watch_action_long_press_hint, label),
535+
Toast.LENGTH_SHORT
536+
).show()
537+
} else {
538+
fireAction(context, state, clickAction, showToast = false)
539+
}
540+
},
541+
onLongPress = if (holdAction != "NONE") {
542+
{
543+
fireAction(context, state, holdAction, showToast = true)
544+
}
545+
} else null
546+
)
547+
},
544548
contentAlignment = Alignment.Center
545549
) {
546550
Icon(

0 commit comments

Comments
 (0)