Skip to content

Bug 2011498 - Add an accelerometer API, implementation, and shake detection#51

Closed
MatthewTighe wants to merge 1 commit intomozilla-firefox:autolandfrom
MatthewTighe:s2s-shake-gesture-detector
Closed

Bug 2011498 - Add an accelerometer API, implementation, and shake detection#51
MatthewTighe wants to merge 1 commit intomozilla-firefox:autolandfrom
MatthewTighe:s2s-shake-gesture-detector

Conversation

@MatthewTighe
Copy link
Contributor

No description provided.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 3, 2026

Warning

The base branch is currently set to main. Please Edit this PR and set the base to autoland.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 3, 2026

View this pull request in Lando to land it once approved.

@MatthewTighe MatthewTighe changed the base branch from main to autoland February 3, 2026 01:13
Copy link
Contributor Author

@MatthewTighe MatthewTighe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple notes in here for follow-ups. My main priority is to get this landing to unblock an end-to-end test of an early implementation.

Comment on lines 115 to 123
val accelerometerFlow = LifecycleAwareSensorManagerEventFlow((requireActivity().getSystemService(Context.SENSOR_SERVICE) as SensorManager))

lifecycle.addObserver(accelerometerFlow)
lifecycleScope.launch {
SampledAccelerometer(accelerometerFlow).detectShakes().collect {
Log.i("shaker", "shake detected")
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove this before landing, just wanted an example of the API

override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
logger("Registering self as sensor listener")
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_UI)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will follow-up on the sampling delay here - this may be too high of a rate for battery reasons and we can likely do something intelligent in terms of increasing the sample rate as necessary.

@MatthewTighe MatthewTighe force-pushed the s2s-shake-gesture-detector branch 4 times, most recently from e75a05d to 7af47ac Compare February 3, 2026 04:23
@segunfamisa segunfamisa self-requested a review February 3, 2026 14:12
Copy link
Contributor

@segunfamisa segunfamisa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR.

This PR is definitely shaping up nicely, thank you.

I have some comments, I highlighted the ones that about naming that are nitpicky - so, please feel free to not pay too much attention to them.

onBufferOverflow = BufferOverflow.DROP_OLDEST,
)

override fun flowEvents(): Flow<Accelerometer.Sample> = _samples
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question:

Do we need to do anything if there's no accelerometer sensor? I think we may need to throw an error that will be handled downstream?

I don't know how we envisage the usage of the sample stream, but we could accidentally have an observer waiting indefinitely if we don't emit anything 🤔

It sounds like we should complete it - either successfully empty or with an error, if we don't have a sensor

Copy link
Contributor Author

@MatthewTighe MatthewTighe Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are you imagining that to look? We don't have a way to complete the flow on the publishing side, so I think this would mean that we'd need to change the AccelerometerEventSource to be a Flow<SensorResult> so that consumers could cancel their collection if we had a SensorResult.Failed.

Is there anything they could do to recover from this case? For our specific use-case, I don't think the resulting behavior is different - we just don't get shake detection.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Let's keep this and see if we need to revisit

mockSensorManager = mock()
mockSensor = mock()
whenever(mockSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)).thenReturn(mockSensor)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to propose using robolectric shadows here instead of mocking to be able to write the tests properly and without mocks.

Here's a sample test I was able to get working:

@RunWith(AndroidJUnit4::class)
@Config(sdk = [28])
class LifecycleAwareSensorManagerEventFlowTest {

    private val testDispatcher = StandardTestDispatcher()

    // from AndroidX
    private lateinit var lifecycleOwner: TestLifecycleOwner

    private lateinit var sensorManager: SensorManager
    private lateinit var shadowSensorManager: ShadowSensorManager

    private lateinit var eventFlow: LifecycleAwareSensorManagerEventFlow

    @Before
    fun setUp() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
        shadowSensorManager = Shadows.shadowOf(sensorManager)
        shadowSensorManager.addSensor(ShadowSensor.newInstance(Sensor.TYPE_ACCELEROMETER))

        eventFlow = LifecycleAwareSensorManagerEventFlow(
            sensorManager = sensorManager,
            logger = { },
        )

        lifecycleOwner = TestLifecycleOwner(Lifecycle.State.INITIALIZED)
        lifecycleOwner.lifecycle.addObserver(eventFlow)
    }

    @Test
    fun `when lifecycle is resumed, sensor events are emitted`() = runTest(testDispatcher) {
        // given the lifecycle is resumed
        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)

        // and we are observing the event flow
        val samples = mutableListOf<Accelerometer.Sample>()
        val eventsJob = launch(testDispatcher) {
            eventFlow.flowEvents()
                .collect { samples.add(it) }
        }

        // when we emit two sensor events
        emitSensorEvent(floatArrayOf(1f, 2f, 3f, 4f))
        emitSensorEvent(floatArrayOf(1f, 2f, 3f, 4f))

        testDispatcher.scheduler.advanceUntilIdle()

        // then we observe two emissions
        assertEquals("Two sensor events are observed", 2, samples.size)
        eventsJob.cancel()
    }

    @Test
    fun `when lifecycle is paused, no sensor events are emitted`() = runTest(testDispatcher) {
        // given that we are observing the events
        val samples = mutableListOf<Accelerometer.Sample>()
        val eventsJob = launch(testDispatcher) {
            eventFlow.flowEvents()
                .collect { samples.add(it) }
        }

        // and two events were emitted when the life cycle was resumed
        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
        emitSensorEvent(floatArrayOf(1f, 2f, 3f, 4f))
        emitSensorEvent(floatArrayOf(1f, 2f, 3f, 4f))

        testDispatcher.scheduler.advanceUntilIdle()

        // when the lifecycle is paused
        lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)

        // and we emit another event
        emitSensorEvent(floatArrayOf(1f, 2f, 3f, 4f))

        testDispatcher.scheduler.advanceUntilIdle()

        // then we observe two emissions
        assertEquals("Expected that only the two prior sensor events were emitted", 2, samples.size)
        eventsJob.cancel()
    }

    private fun emitSensorEvent(values: FloatArray = floatArrayOf(1f, 2f, 3f, 4f)) {
        val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
        shadowSensorManager.sendSensorEventToListeners(
            SensorEventBuilder.newBuilder()
                .setSensor(requireNotNull(sensor))
                .setValues(values)
                .setTimestamp(1234)
                .build(),
        )
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not exactly sure why, but applying these tests for me was racy because the launched collection didn't start in time. I had to runCurrent on the test dispatcher to ensure collection started eagerly

import org.junit.Assert.assertEquals
import org.junit.Test

class SampledAccelerometerTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

private val flow: AccelerometerEventFlow,
) : Accelerometer {

override fun gravityNormalizedSamples(): Flow<Accelerometer.Sample> = flow
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we going to have the sampling happen later? Or it has already happened somewhere? 😅

Comment on lines 37 to 41
enum class ShakeSensitivity {
Low,
Medium,
High,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we make this a class, and we provided Low, High and Medium values as this:

@JvmInline
value class ShakeSensitivity(val threshold: Float) {
  companion object {
    val Low = ShakeSensitivity(threshold = 3.2f)
    val Medium = ShakeSensitivity(threshold = 2.7f)
    ....
  }
}

The benefit of this idea is that it gives room for consumers of this class to be able to use their own in-between sensitivity thresholds without us having to change our API, and we don't have to manually map the given sensitivity into a threshold in the next() function besides the default ones we've defined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this idea!

@MatthewTighe MatthewTighe force-pushed the s2s-shake-gesture-detector branch from 7af47ac to 3c41be0 Compare February 5, 2026 01:07
Copy link
Contributor

@segunfamisa segunfamisa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot. Changes look great to me. ✅

@MatthewTighe MatthewTighe force-pushed the s2s-shake-gesture-detector branch from 3c41be0 to b3c160b Compare February 5, 2026 19:25
@lando-prod-mozilla
Copy link

Pull request closed by commit c63d58e

lando-prod-mozilla bot pushed a commit that referenced this pull request Feb 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants