Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 90db903f authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez Committed by Android (Google) Code Review
Browse files

Merge "Introducing API to produce slider events." into udc-qpr-dev

parents b6a6a939 75e5a76d
Loading
Loading
Loading
Loading
+68 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.haptics.slider

import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

/** An event producer for a Seekable element such as the Android [SeekBar] */
class SeekableSliderEventProducer : SliderEventProducer, OnSeekBarChangeListener {

    /** The current event reported by a SeekBar */
    private val _currentEvent = MutableStateFlow(SliderEvent(SliderEventType.NOTHING, 0f))

    override fun produceEvents(): Flow<SliderEvent> = _currentEvent.asStateFlow()

    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
        val eventType =
            if (fromUser) SliderEventType.PROGRESS_CHANGE_BY_USER
            else SliderEventType.PROGRESS_CHANGE_BY_PROGRAM

        _currentEvent.value = SliderEvent(eventType, normalizeProgress(seekBar, progress))
    }

    /**
     * Normalize the integer progress of a SeekBar to the range from 0F to 1F.
     *
     * @param[seekBar] The SeekBar that reports a progress.
     * @param[progress] The integer progress of the SeekBar within its min and max values.
     * @return The progress in the range from 0F to 1F.
     */
    private fun normalizeProgress(seekBar: SeekBar, progress: Int): Float {
        if (seekBar.max == seekBar.min) {
            return 1.0f
        }
        val range = seekBar.max - seekBar.min
        return (progress - seekBar.min) / range.toFloat()
    }

    override fun onStartTrackingTouch(seekBar: SeekBar) {
        _currentEvent.update { previousEvent ->
            SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, previousEvent.currentProgress)
        }
    }

    override fun onStopTrackingTouch(seekBar: SeekBar) {
        _currentEvent.update { previousEvent ->
            SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, previousEvent.currentProgress)
        }
    }
}
+31 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.haptics.slider

import androidx.annotation.FloatRange

/**
 * An event arising from a slider.
 *
 * @property[type] The type of event. Must be one of [SliderEventType].
 * @property[currentProgress] The current progress of the slider normalized to the range between 0F
 *   and 1F (inclusive).
 */
data class SliderEvent(
    val type: SliderEventType,
    @FloatRange(from = 0.0, to = 1.0) val currentProgress: Float
)
+30 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.haptics.slider

import kotlinx.coroutines.flow.Flow

/** Defines a producer of [SliderEvent] to be consumed as a [Flow] */
interface SliderEventProducer {

    /**
     * Produce a stream of [SliderEvent]
     *
     * @return A [Flow] of [SliderEvent] produced from the operation of a slider.
     */
    fun produceEvents(): Flow<SliderEvent>
}
+33 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.haptics.slider

/** The type of a [SliderEvent]. */
enum class SliderEventType {
    /* No event. */
    NOTHING,
    /* The slider has captured a touch input and is tracking touch events. */
    STARTED_TRACKING_TOUCH,
    /* The slider progress is changing due to user touch input. */
    PROGRESS_CHANGE_BY_USER,
    /* The slider progress is changing programmatically. */
    PROGRESS_CHANGE_BY_PROGRAM,
    /* The slider has stopped tracking touch events. */
    STOPPED_TRACKING_TOUCH,
    /* The external (not touch) stimulus that was modifying the slider progress has stopped. */
    EXTERNAL_STIMULUS_RELEASE,
}
+126 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.haptics.slider

import android.widget.SeekBar
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class SeekableSliderEventProducerTest : SysuiTestCase() {

    private val seekBar = SeekBar(mContext)
    private val eventProducer = SeekableSliderEventProducer()
    private val eventFlow = eventProducer.produceEvents()

    @Test
    fun onStartTrackingTouch_noProgress_trackingTouchEventProduced() = runTest {
        val latest by collectLastValue(eventFlow)

        eventProducer.onStartTrackingTouch(seekBar)

        assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0F), latest)
    }

    @Test
    fun onStopTrackingTouch_noProgress_StoppedTrackingTouchEventProduced() = runTest {
        val latest by collectLastValue(eventFlow)

        eventProducer.onStopTrackingTouch(seekBar)

        assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0F), latest)
    }

    @Test
    fun onProgressChangeByUser_changeByUserEventProduced_withNormalizedProgress() = runTest {
        val progress = 50
        val latest by collectLastValue(eventFlow)

        eventProducer.onProgressChanged(seekBar, progress, true)

        assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5F), latest)
    }

    @Test
    fun onProgressChangeByUser_zeroWidthSlider_changeByUserEventProduced_withMaxProgress() =
        runTest {
            // No-width slider where the min and max values are the same
            seekBar.min = 100
            seekBar.max = 100
            val progress = 50
            val latest by collectLastValue(eventFlow)

            eventProducer.onProgressChanged(seekBar, progress, true)

            assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 1.0F), latest)
        }

    @Test
    fun onProgressChangeByProgram_changeByProgramEventProduced_withNormalizedProgress() = runTest {
        val progress = 50
        val latest by collectLastValue(eventFlow)

        eventProducer.onProgressChanged(seekBar, progress, false)

        assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 0.5F), latest)
    }

    @Test
    fun onProgressChangeByProgram_zeroWidthSlider_changeByProgramEventProduced_withMaxProgress() =
        runTest {
            // No-width slider where the min and max values are the same
            seekBar.min = 100
            seekBar.max = 100
            val progress = 50
            val latest by collectLastValue(eventFlow)

            eventProducer.onProgressChanged(seekBar, progress, false)

            assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 1.0F), latest)
        }

    @Test
    fun onStartTrackingTouch_afterProgress_trackingTouchEventProduced_withNormalizedProgress() =
        runTest {
            val progress = 50
            val latest by collectLastValue(eventFlow)

            eventProducer.onProgressChanged(seekBar, progress, true)
            eventProducer.onStartTrackingTouch(seekBar)

            assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0.5F), latest)
        }

    @Test
    fun onStopTrackingTouch_afterProgress_stopTrackingTouchEventProduced_withNormalizedProgress() =
        runTest {
            val progress = 50
            val latest by collectLastValue(eventFlow)

            eventProducer.onProgressChanged(seekBar, progress, true)
            eventProducer.onStopTrackingTouch(seekBar)

            assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5F), latest)
        }
}